├── .gitignore ├── LICENSE ├── README.md ├── jmeter ├── miaosha.html ├── miaosha.openapi.json ├── 下单测试.jmx └── 乐观锁+令牌限流下单测试.jmx ├── miaosha-dao ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── cn │ │ │ └── monitor4all │ │ │ └── miaoshadao │ │ │ ├── MiaoshaDaoApplication.java │ │ │ ├── dao │ │ │ ├── Stock.java │ │ │ ├── StockExample.java │ │ │ ├── StockOrder.java │ │ │ ├── StockOrderExample.java │ │ │ └── User.java │ │ │ ├── mapper │ │ │ ├── StockMapper.java │ │ │ ├── StockOrderMapper.java │ │ │ └── UserMapper.java │ │ │ └── utils │ │ │ └── CacheKey.java │ └── resources │ │ ├── application.properties │ │ ├── generatorConfig.xml │ │ └── mapper │ │ ├── StockMapper.xml │ │ ├── StockOrderMapper.xml │ │ └── UserMapper.xml │ └── test │ └── java │ └── cn │ └── monitor4all │ └── miaoshadao │ └── MiaoshaDaoApplicationTests.java ├── miaosha-job ├── pom.xml └── src │ └── main │ └── java │ └── job │ └── CanalClient.java ├── miaosha-service ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── cn │ │ │ └── monitor4all │ │ │ └── miaoshaservice │ │ │ ├── MiaoshaServiceApplication.java │ │ │ └── service │ │ │ ├── OrderService.java │ │ │ ├── OrderServiceImpl.java │ │ │ ├── StockService.java │ │ │ ├── StockServiceImpl.java │ │ │ ├── UserService.java │ │ │ └── UserServiceImpl.java │ └── resources │ │ ├── application.properties │ │ └── generatorConfig.xml │ └── test │ └── java │ └── cn │ └── monitor4all │ └── miaoshaservice │ └── MiaoshaServiceApplicationTests.java ├── miaosha-web ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── cn │ │ │ └── monitor4all │ │ │ └── miaoshaweb │ │ │ ├── MiaoshaWebApplication.java │ │ │ ├── config │ │ │ └── RabbitMqConfig.java │ │ │ ├── controller │ │ │ ├── OrderController.java │ │ │ └── StockController.java │ │ │ └── receiver │ │ │ ├── DelCacheReceiver.java │ │ │ └── OrderMqReceiver.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── cn │ └── monitor4all │ └── miaoshaweb │ └── MiaoshaWebApplicationTests.java ├── miaosha.sql ├── pom.xml └── src └── main └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | /miaosha-dao/target/ 33 | -------------------------------------------------------------------------------- /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 | 该项目为基于SpringBoot的简易秒杀系统实战代码 4 | 5 | ## 申明 6 | 7 | 本仓库代码遵守Apache License 2.0,文章遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 8 | 9 | 洗稿,抄袭代码者,自重!已经发现多个擅自将本仓库代码作为文字、视频教程并公开售卖的技术博主,严重违反本仓库版权协议,请网友积极反馈并在各平台举报。目前本代码仓库没有制作任何视频教程,只有个人博客上发表的文字教程。 10 | 11 | 本人全网笔名:蛮三刀酱 12 | 13 | 微信公众号:后端技术漫谈 14 | 15 | ## 对应教程 16 | 17 | [【秒杀系统】零基础上手秒杀系统(一):防止超卖](https://mp.weixin.qq.com/s?__biz=MzU1NTA0NTEwMg==&mid=2247484174&idx=1&sn=235af7ead49a7d33e7fab52e05d5021f&lang=zh_CN#rd) 18 | 19 | [【秒杀系统】零基础上手秒杀系统(二):令牌桶限流 + 再谈超卖](https://mp.weixin.qq.com/s?__biz=MzU1NTA0NTEwMg==&mid=2247484178&idx=1&sn=f4d8072b5408b08f983cae26a6ce1cf5&lang=zh_CN#rd) 20 | 21 | [【秒杀系统】零基础上手秒杀系统(三):抢购接口隐藏 + 单用户限制频率](https://mp.weixin.qq.com/s?__biz=MzU1NTA0NTEwMg==&mid=2247484184&idx=1&sn=8b878e9e730a6e4da27ed336c8201c92&lang=zh_CN#rd) 22 | 23 | [【秒杀系统】零基础上手秒杀系统(四):缓存与数据库双写问题的争议](https://mp.weixin.qq.com/s?__biz=MzU1NTA0NTEwMg==&mid=2247484200&idx=1&sn=6b6c7251ee83fe8ef9201373aafcffdd&chksm=fbdb1aa9ccac93bfe26655f89056b0d25b3a536f6b11148878fe96ffdf1d8349d44659cad784&token=841068032&lang=zh_CN#rd) 24 | 25 | [【秒杀系统】零基础上手秒杀系统番外篇:阿里开源MySQL中间件Canal快速入门](https://mp.weixin.qq.com/s?__biz=MzU1NTA0NTEwMg==&mid=2247484273&idx=1&sn=7fec41a40e763df094c0dd675330808a&chksm=fbdb1af0ccac93e676c2a0c6aeb1ff3edfe43b30969a7c1bbe19ccf7270acd6e41e6812caf0d&lang=zh_CN#rd) 26 | 27 | [【秒杀系统】零基础上手秒杀系统(五):如何优雅的实现订单异步处理](https://mp.weixin.qq.com/s?__biz=MzU1NTA0NTEwMg==&mid=2247484580&idx=1&sn=5092e7d109b0f9bb9ed604fa03ac82cb&chksm=fbdb1d25ccac94338e39a6a36fab10dc36dd6a91a4e3cb0db1559cb82448865c4510b80a41ef&token=845103815&lang=zh_CN#rd) 28 | 29 | 30 | ## 项目使用简介 31 | 32 | 项目是SpringBoot工程,并且是父子工程,直接导入IDEA即可使用。 33 | 34 | 1. 导入miaosha.sql文件到你的MySQL数据库 35 | 36 | 2. 配置application.properties文件,修改为你的数据库链接地址 37 | 38 | 3. mvn clean install最外层的父工程(pom.xml) 39 | 40 | 4. 运行miaosha-web,在POSTMAN或者浏览器直接访问请求链接即可。 41 | -------------------------------------------------------------------------------- /jmeter/miaosha.openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "本地测试", 5 | "description": "", 6 | "version": "1.0.0" 7 | }, 8 | "tags": [ 9 | { 10 | "name": "本地测试" 11 | } 12 | ], 13 | "paths": { 14 | "/createWrongOrder/{sid}": { 15 | "get": { 16 | "summary": "普通下单接口", 17 | "x-apifox-folder": "本地测试", 18 | "x-apifox-status": "developing", 19 | "deprecated": false, 20 | "description": "", 21 | "tags": [ 22 | "本地测试" 23 | ], 24 | "parameters": [ 25 | { 26 | "name": "sid", 27 | "in": "path", 28 | "description": "", 29 | "required": true, 30 | "example": "1", 31 | "schema": { 32 | "type": "string" 33 | } 34 | } 35 | ], 36 | "responses": { 37 | "200": { 38 | "description": "成功", 39 | "content": { 40 | "application/json": { 41 | "schema": { 42 | "type": "object", 43 | "properties": {}, 44 | "x-apifox-orders": [], 45 | "x-apifox-ignore-properties": [] 46 | } 47 | } 48 | } 49 | } 50 | }, 51 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966622-run" 52 | } 53 | }, 54 | "/createOptimisticOrder/{sid}": { 55 | "get": { 56 | "summary": "乐观锁控制下单接口", 57 | "x-apifox-folder": "本地测试", 58 | "x-apifox-status": "developing", 59 | "deprecated": false, 60 | "description": "", 61 | "tags": [ 62 | "本地测试" 63 | ], 64 | "parameters": [ 65 | { 66 | "name": "sid", 67 | "in": "path", 68 | "description": "", 69 | "required": true, 70 | "example": "1", 71 | "schema": { 72 | "type": "string" 73 | } 74 | } 75 | ], 76 | "responses": { 77 | "200": { 78 | "description": "成功", 79 | "content": { 80 | "application/json": { 81 | "schema": { 82 | "type": "object", 83 | "properties": {}, 84 | "x-apifox-orders": [], 85 | "x-apifox-ignore-properties": [] 86 | } 87 | } 88 | } 89 | } 90 | }, 91 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966624-run" 92 | } 93 | }, 94 | "/createPessimisticOrder/{sid}": { 95 | "get": { 96 | "summary": "乐观锁+非阻塞限流令牌控制下单接口", 97 | "x-apifox-folder": "本地测试", 98 | "x-apifox-status": "developing", 99 | "deprecated": false, 100 | "description": "", 101 | "tags": [ 102 | "本地测试" 103 | ], 104 | "parameters": [ 105 | { 106 | "name": "sid", 107 | "in": "path", 108 | "description": "", 109 | "required": true, 110 | "example": "1", 111 | "schema": { 112 | "type": "string" 113 | } 114 | } 115 | ], 116 | "responses": { 117 | "200": { 118 | "description": "成功", 119 | "content": { 120 | "application/json": { 121 | "schema": { 122 | "type": "object", 123 | "properties": {}, 124 | "x-apifox-orders": [], 125 | "x-apifox-ignore-properties": [] 126 | } 127 | } 128 | } 129 | } 130 | }, 131 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966628-run" 132 | } 133 | }, 134 | "/createOrderWithMq": { 135 | "get": { 136 | "summary": "异步处理订单", 137 | "x-apifox-folder": "本地测试", 138 | "x-apifox-status": "developing", 139 | "deprecated": false, 140 | "description": "", 141 | "tags": [ 142 | "本地测试" 143 | ], 144 | "parameters": [ 145 | { 146 | "name": "userId", 147 | "in": "query", 148 | "description": "", 149 | "required": true, 150 | "example": "1", 151 | "schema": { 152 | "type": "string" 153 | } 154 | }, 155 | { 156 | "name": "sid", 157 | "in": "query", 158 | "description": "", 159 | "required": true, 160 | "example": "1", 161 | "schema": { 162 | "type": "string" 163 | } 164 | } 165 | ], 166 | "responses": { 167 | "200": { 168 | "description": "成功", 169 | "content": { 170 | "application/json": { 171 | "schema": { 172 | "type": "object", 173 | "properties": {}, 174 | "x-apifox-orders": [], 175 | "x-apifox-ignore-properties": [] 176 | } 177 | } 178 | } 179 | } 180 | }, 181 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966627-run" 182 | } 183 | }, 184 | "/createOrderWithVerifiedUrl": { 185 | "get": { 186 | "summary": "验证下单", 187 | "x-apifox-folder": "本地测试", 188 | "x-apifox-status": "developing", 189 | "deprecated": false, 190 | "description": "", 191 | "tags": [ 192 | "本地测试" 193 | ], 194 | "parameters": [ 195 | { 196 | "name": "userId", 197 | "in": "query", 198 | "description": "", 199 | "required": true, 200 | "example": "1", 201 | "schema": { 202 | "type": "string" 203 | } 204 | }, 205 | { 206 | "name": "sid", 207 | "in": "query", 208 | "description": "", 209 | "required": true, 210 | "example": "1", 211 | "schema": { 212 | "type": "string" 213 | } 214 | }, 215 | { 216 | "name": "verifyHash", 217 | "in": "query", 218 | "description": "", 219 | "required": true, 220 | "example": "d4ff4c458da98f69b880dd79c8a30bcf", 221 | "schema": { 222 | "type": "string" 223 | } 224 | } 225 | ], 226 | "responses": { 227 | "200": { 228 | "description": "成功", 229 | "content": { 230 | "application/json": { 231 | "schema": { 232 | "type": "object", 233 | "properties": {}, 234 | "x-apifox-orders": [], 235 | "x-apifox-ignore-properties": [] 236 | } 237 | } 238 | } 239 | } 240 | }, 241 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966629-run" 242 | } 243 | }, 244 | "/createUserOrderWithMq": { 245 | "get": { 246 | "summary": "异步处理订单 限制抢购", 247 | "x-apifox-folder": "本地测试", 248 | "x-apifox-status": "developing", 249 | "deprecated": false, 250 | "description": "", 251 | "tags": [ 252 | "本地测试" 253 | ], 254 | "parameters": [ 255 | { 256 | "name": "userId", 257 | "in": "query", 258 | "description": "", 259 | "required": true, 260 | "example": "1", 261 | "schema": { 262 | "type": "string" 263 | } 264 | }, 265 | { 266 | "name": "sid", 267 | "in": "query", 268 | "description": "", 269 | "required": true, 270 | "example": "1", 271 | "schema": { 272 | "type": "string" 273 | } 274 | } 275 | ], 276 | "responses": { 277 | "200": { 278 | "description": "成功", 279 | "content": { 280 | "application/json": { 281 | "schema": { 282 | "type": "object", 283 | "properties": {}, 284 | "x-apifox-orders": [], 285 | "x-apifox-ignore-properties": [] 286 | } 287 | } 288 | } 289 | } 290 | }, 291 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966630-run" 292 | } 293 | }, 294 | "/getVerifyHash": { 295 | "get": { 296 | "summary": "下单前用户获取验证值", 297 | "x-apifox-folder": "本地测试", 298 | "x-apifox-status": "developing", 299 | "deprecated": false, 300 | "description": "", 301 | "tags": [ 302 | "本地测试" 303 | ], 304 | "parameters": [ 305 | { 306 | "name": "userId", 307 | "in": "query", 308 | "description": "", 309 | "required": true, 310 | "example": "1", 311 | "schema": { 312 | "type": "string" 313 | } 314 | }, 315 | { 316 | "name": "sid", 317 | "in": "query", 318 | "description": "", 319 | "required": true, 320 | "example": "1", 321 | "schema": { 322 | "type": "string" 323 | } 324 | } 325 | ], 326 | "responses": { 327 | "200": { 328 | "description": "成功", 329 | "content": { 330 | "application/json": { 331 | "schema": { 332 | "type": "object", 333 | "properties": {}, 334 | "x-apifox-orders": [], 335 | "x-apifox-ignore-properties": [] 336 | } 337 | } 338 | } 339 | } 340 | }, 341 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966631-run" 342 | } 343 | }, 344 | "/createOrderWithVerifiedUrlAndLimit": { 345 | "get": { 346 | "summary": "要求验证的抢购接口 + 单用户限制访问频率", 347 | "x-apifox-folder": "本地测试", 348 | "x-apifox-status": "developing", 349 | "deprecated": false, 350 | "description": "", 351 | "tags": [ 352 | "本地测试" 353 | ], 354 | "parameters": [ 355 | { 356 | "name": "userId", 357 | "in": "query", 358 | "description": "", 359 | "required": true, 360 | "example": "1", 361 | "schema": { 362 | "type": "string" 363 | } 364 | }, 365 | { 366 | "name": "sid", 367 | "in": "query", 368 | "description": "", 369 | "required": true, 370 | "example": "1", 371 | "schema": { 372 | "type": "string" 373 | } 374 | }, 375 | { 376 | "name": "verifyHash", 377 | "in": "query", 378 | "description": "", 379 | "required": true, 380 | "example": "d4ff4c458da98f69b880dd79c8a30bcf", 381 | "schema": { 382 | "type": "string" 383 | } 384 | } 385 | ], 386 | "responses": { 387 | "200": { 388 | "description": "成功", 389 | "content": { 390 | "application/json": { 391 | "schema": { 392 | "type": "object", 393 | "properties": {}, 394 | "x-apifox-orders": [], 395 | "x-apifox-ignore-properties": [] 396 | } 397 | } 398 | } 399 | } 400 | }, 401 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966632-run" 402 | } 403 | }, 404 | "/createOrderWithCacheV2/{sid}": { 405 | "get": { 406 | "summary": "先更新数据库,再删缓存", 407 | "x-apifox-folder": "本地测试", 408 | "x-apifox-status": "developing", 409 | "deprecated": false, 410 | "description": "", 411 | "tags": [ 412 | "本地测试" 413 | ], 414 | "parameters": [ 415 | { 416 | "name": "sid", 417 | "in": "path", 418 | "description": "", 419 | "required": true, 420 | "example": "1", 421 | "schema": { 422 | "type": "string" 423 | } 424 | } 425 | ], 426 | "responses": { 427 | "200": { 428 | "description": "成功", 429 | "content": { 430 | "application/json": { 431 | "schema": { 432 | "type": "object", 433 | "properties": {}, 434 | "x-apifox-orders": [], 435 | "x-apifox-ignore-properties": [] 436 | } 437 | } 438 | } 439 | } 440 | }, 441 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966633-run" 442 | } 443 | }, 444 | "/createOrderWithCacheV1/{sid}": { 445 | "get": { 446 | "summary": "先删除缓存,再更新数据库", 447 | "x-apifox-folder": "本地测试", 448 | "x-apifox-status": "developing", 449 | "deprecated": false, 450 | "description": "", 451 | "tags": [ 452 | "本地测试" 453 | ], 454 | "parameters": [ 455 | { 456 | "name": "sid", 457 | "in": "path", 458 | "description": "", 459 | "required": true, 460 | "example": "1", 461 | "schema": { 462 | "type": "string" 463 | } 464 | } 465 | ], 466 | "responses": { 467 | "200": { 468 | "description": "成功", 469 | "content": { 470 | "application/json": { 471 | "schema": { 472 | "type": "object", 473 | "properties": {}, 474 | "x-apifox-orders": [], 475 | "x-apifox-ignore-properties": [] 476 | } 477 | } 478 | } 479 | } 480 | }, 481 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966634-run" 482 | } 483 | }, 484 | "/createOrderWithCacheV3/{sid}": { 485 | "get": { 486 | "summary": "先删除缓存,再更新数据库,缓存延时双删", 487 | "x-apifox-folder": "本地测试", 488 | "x-apifox-status": "developing", 489 | "deprecated": false, 490 | "description": "", 491 | "tags": [ 492 | "本地测试" 493 | ], 494 | "parameters": [ 495 | { 496 | "name": "sid", 497 | "in": "path", 498 | "description": "", 499 | "required": true, 500 | "example": "1", 501 | "schema": { 502 | "type": "string" 503 | } 504 | } 505 | ], 506 | "responses": { 507 | "200": { 508 | "description": "成功", 509 | "content": { 510 | "application/json": { 511 | "schema": { 512 | "type": "object", 513 | "properties": {}, 514 | "x-apifox-orders": [], 515 | "x-apifox-ignore-properties": [] 516 | } 517 | } 518 | } 519 | } 520 | }, 521 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966635-run" 522 | } 523 | }, 524 | "/createOrderWithCacheV4/{sid}": { 525 | "get": { 526 | "summary": "先更新数据库,再删缓存,删除缓存失败重试,通知消息队列", 527 | "x-apifox-folder": "本地测试", 528 | "x-apifox-status": "developing", 529 | "deprecated": false, 530 | "description": "", 531 | "tags": [ 532 | "本地测试" 533 | ], 534 | "parameters": [ 535 | { 536 | "name": "sid", 537 | "in": "path", 538 | "description": "", 539 | "required": true, 540 | "example": "1", 541 | "schema": { 542 | "type": "string" 543 | } 544 | } 545 | ], 546 | "responses": { 547 | "200": { 548 | "description": "成功", 549 | "content": { 550 | "application/json": { 551 | "schema": { 552 | "type": "object", 553 | "properties": {}, 554 | "x-apifox-orders": [], 555 | "x-apifox-ignore-properties": [] 556 | } 557 | } 558 | } 559 | } 560 | }, 561 | "x-run-in-apifox": "https://www.apifox.cn/web/project/1589557/apis/api-38966637-run" 562 | } 563 | } 564 | }, 565 | "components": { 566 | "schemas": {} 567 | } 568 | } -------------------------------------------------------------------------------- /jmeter/下单测试.jmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | true 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | continue 17 | 18 | false 19 | 1 20 | 21 | 200 22 | 1 23 | false 24 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | 127.0.0.1 34 | 8080 35 | http 36 | 37 | /createWrongOrder/1 38 | GET 39 | true 40 | false 41 | true 42 | false 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | false 56 | 57 | saveConfig 58 | 59 | 60 | true 61 | true 62 | true 63 | 64 | true 65 | true 66 | true 67 | true 68 | false 69 | true 70 | true 71 | false 72 | false 73 | false 74 | true 75 | false 76 | false 77 | false 78 | true 79 | 0 80 | true 81 | true 82 | true 83 | true 84 | true 85 | true 86 | 87 | 88 | 89 | 90 | 91 | 92 | false 93 | 94 | saveConfig 95 | 96 | 97 | true 98 | true 99 | true 100 | 101 | true 102 | true 103 | true 104 | true 105 | false 106 | true 107 | true 108 | false 109 | false 110 | false 111 | true 112 | false 113 | false 114 | false 115 | true 116 | 0 117 | true 118 | true 119 | true 120 | true 121 | true 122 | true 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /jmeter/乐观锁+令牌限流下单测试.jmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | true 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | continue 17 | 18 | false 19 | 1 20 | 21 | 1 22 | 1 23 | false 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 127.0.0.1 34 | 8080 35 | http 36 | 37 | /createOptimisticOrder/%24%7Bsid%7D 38 | GET 39 | true 40 | false 41 | true 42 | false 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | false 57 | 58 | saveConfig 59 | 60 | 61 | true 62 | true 63 | true 64 | 65 | true 66 | true 67 | true 68 | true 69 | false 70 | true 71 | true 72 | false 73 | false 74 | false 75 | true 76 | false 77 | false 78 | false 79 | true 80 | 0 81 | true 82 | true 83 | true 84 | true 85 | true 86 | true 87 | 88 | 89 | 90 | 91 | 92 | 93 | false 94 | 95 | saveConfig 96 | 97 | 98 | true 99 | true 100 | true 101 | 102 | true 103 | true 104 | true 105 | true 106 | false 107 | true 108 | true 109 | false 110 | false 111 | false 112 | true 113 | false 114 | false 115 | false 116 | true 117 | 0 118 | true 119 | true 120 | true 121 | true 122 | true 123 | true 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /miaosha-dao/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /miaosha-dao/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | cn.monitor4all 7 | miaosha 8 | 1.0.0-SNAPSHOT 9 | 10 | miaosha-dao 11 | 12 | 13 | 1.8 14 | 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter 20 | 21 | 22 | 23 | org.mybatis.spring.boot 24 | mybatis-spring-boot-starter 25 | 1.2.0 26 | 27 | 28 | 29 | mysql 30 | mysql-connector-java 31 | 5.1.6 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-test 37 | test 38 | 39 | 40 | org.junit.vintage 41 | junit-vintage-engine 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | maven-compiler-plugin 51 | 52 | 1.8 53 | 1.8 54 | 55 | 56 | 57 | 58 | org.mybatis.generator 59 | mybatis-generator-maven-plugin 60 | 1.3.2 61 | 62 | true 63 | true 64 | 65 | 66 | 67 | 68 | mysql 69 | mysql-connector-java 70 | 5.1.6 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /miaosha-dao/src/main/java/cn/monitor4all/miaoshadao/MiaoshaDaoApplication.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshadao; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class MiaoshaDaoApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(MiaoshaDaoApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /miaosha-dao/src/main/java/cn/monitor4all/miaoshadao/dao/Stock.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshadao.dao; 2 | 3 | public class Stock { 4 | private Integer id; 5 | 6 | private String name; 7 | 8 | private Integer count; 9 | 10 | private Integer sale; 11 | 12 | private Integer version; 13 | 14 | public Integer getId() { 15 | return id; 16 | } 17 | 18 | public void setId(Integer id) { 19 | this.id = id; 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void setName(String name) { 27 | this.name = name == null ? null : name.trim(); 28 | } 29 | 30 | public Integer getCount() { 31 | return count; 32 | } 33 | 34 | public void setCount(Integer count) { 35 | this.count = count; 36 | } 37 | 38 | public Integer getSale() { 39 | return sale; 40 | } 41 | 42 | public void setSale(Integer sale) { 43 | this.sale = sale; 44 | } 45 | 46 | public Integer getVersion() { 47 | return version; 48 | } 49 | 50 | public void setVersion(Integer version) { 51 | this.version = version; 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | StringBuilder sb = new StringBuilder(); 57 | sb.append(getClass().getSimpleName()); 58 | sb.append(" ["); 59 | sb.append("Hash = ").append(hashCode()); 60 | sb.append(", id=").append(id); 61 | sb.append(", name=").append(name); 62 | sb.append(", count=").append(count); 63 | sb.append(", sale=").append(sale); 64 | sb.append(", version=").append(version); 65 | sb.append("]"); 66 | return sb.toString(); 67 | } 68 | } -------------------------------------------------------------------------------- /miaosha-dao/src/main/java/cn/monitor4all/miaoshadao/dao/StockExample.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshadao.dao; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class StockExample { 7 | protected String orderByClause; 8 | 9 | protected boolean distinct; 10 | 11 | protected List oredCriteria; 12 | 13 | public StockExample() { 14 | oredCriteria = new ArrayList(); 15 | } 16 | 17 | public void setOrderByClause(String orderByClause) { 18 | this.orderByClause = orderByClause; 19 | } 20 | 21 | public String getOrderByClause() { 22 | return orderByClause; 23 | } 24 | 25 | public void setDistinct(boolean distinct) { 26 | this.distinct = distinct; 27 | } 28 | 29 | public boolean isDistinct() { 30 | return distinct; 31 | } 32 | 33 | public List getOredCriteria() { 34 | return oredCriteria; 35 | } 36 | 37 | public void or(Criteria criteria) { 38 | oredCriteria.add(criteria); 39 | } 40 | 41 | public Criteria or() { 42 | Criteria criteria = createCriteriaInternal(); 43 | oredCriteria.add(criteria); 44 | return criteria; 45 | } 46 | 47 | public Criteria createCriteria() { 48 | Criteria criteria = createCriteriaInternal(); 49 | if (oredCriteria.size() == 0) { 50 | oredCriteria.add(criteria); 51 | } 52 | return criteria; 53 | } 54 | 55 | protected Criteria createCriteriaInternal() { 56 | Criteria criteria = new Criteria(); 57 | return criteria; 58 | } 59 | 60 | public void clear() { 61 | oredCriteria.clear(); 62 | orderByClause = null; 63 | distinct = false; 64 | } 65 | 66 | protected abstract static class GeneratedCriteria { 67 | protected List criteria; 68 | 69 | protected GeneratedCriteria() { 70 | super(); 71 | criteria = new ArrayList(); 72 | } 73 | 74 | public boolean isValid() { 75 | return criteria.size() > 0; 76 | } 77 | 78 | public List getAllCriteria() { 79 | return criteria; 80 | } 81 | 82 | public List getCriteria() { 83 | return criteria; 84 | } 85 | 86 | protected void addCriterion(String condition) { 87 | if (condition == null) { 88 | throw new RuntimeException("Value for condition cannot be null"); 89 | } 90 | criteria.add(new Criterion(condition)); 91 | } 92 | 93 | protected void addCriterion(String condition, Object value, String property) { 94 | if (value == null) { 95 | throw new RuntimeException("Value for " + property + " cannot be null"); 96 | } 97 | criteria.add(new Criterion(condition, value)); 98 | } 99 | 100 | protected void addCriterion(String condition, Object value1, Object value2, String property) { 101 | if (value1 == null || value2 == null) { 102 | throw new RuntimeException("Between values for " + property + " cannot be null"); 103 | } 104 | criteria.add(new Criterion(condition, value1, value2)); 105 | } 106 | 107 | public Criteria andIdIsNull() { 108 | addCriterion("id is null"); 109 | return (Criteria) this; 110 | } 111 | 112 | public Criteria andIdIsNotNull() { 113 | addCriterion("id is not null"); 114 | return (Criteria) this; 115 | } 116 | 117 | public Criteria andIdEqualTo(Integer value) { 118 | addCriterion("id =", value, "id"); 119 | return (Criteria) this; 120 | } 121 | 122 | public Criteria andIdNotEqualTo(Integer value) { 123 | addCriterion("id <>", value, "id"); 124 | return (Criteria) this; 125 | } 126 | 127 | public Criteria andIdGreaterThan(Integer value) { 128 | addCriterion("id >", value, "id"); 129 | return (Criteria) this; 130 | } 131 | 132 | public Criteria andIdGreaterThanOrEqualTo(Integer value) { 133 | addCriterion("id >=", value, "id"); 134 | return (Criteria) this; 135 | } 136 | 137 | public Criteria andIdLessThan(Integer value) { 138 | addCriterion("id <", value, "id"); 139 | return (Criteria) this; 140 | } 141 | 142 | public Criteria andIdLessThanOrEqualTo(Integer value) { 143 | addCriterion("id <=", value, "id"); 144 | return (Criteria) this; 145 | } 146 | 147 | public Criteria andIdIn(List values) { 148 | addCriterion("id in", values, "id"); 149 | return (Criteria) this; 150 | } 151 | 152 | public Criteria andIdNotIn(List values) { 153 | addCriterion("id not in", values, "id"); 154 | return (Criteria) this; 155 | } 156 | 157 | public Criteria andIdBetween(Integer value1, Integer value2) { 158 | addCriterion("id between", value1, value2, "id"); 159 | return (Criteria) this; 160 | } 161 | 162 | public Criteria andIdNotBetween(Integer value1, Integer value2) { 163 | addCriterion("id not between", value1, value2, "id"); 164 | return (Criteria) this; 165 | } 166 | 167 | public Criteria andNameIsNull() { 168 | addCriterion("name is null"); 169 | return (Criteria) this; 170 | } 171 | 172 | public Criteria andNameIsNotNull() { 173 | addCriterion("name is not null"); 174 | return (Criteria) this; 175 | } 176 | 177 | public Criteria andNameEqualTo(String value) { 178 | addCriterion("name =", value, "name"); 179 | return (Criteria) this; 180 | } 181 | 182 | public Criteria andNameNotEqualTo(String value) { 183 | addCriterion("name <>", value, "name"); 184 | return (Criteria) this; 185 | } 186 | 187 | public Criteria andNameGreaterThan(String value) { 188 | addCriterion("name >", value, "name"); 189 | return (Criteria) this; 190 | } 191 | 192 | public Criteria andNameGreaterThanOrEqualTo(String value) { 193 | addCriterion("name >=", value, "name"); 194 | return (Criteria) this; 195 | } 196 | 197 | public Criteria andNameLessThan(String value) { 198 | addCriterion("name <", value, "name"); 199 | return (Criteria) this; 200 | } 201 | 202 | public Criteria andNameLessThanOrEqualTo(String value) { 203 | addCriterion("name <=", value, "name"); 204 | return (Criteria) this; 205 | } 206 | 207 | public Criteria andNameLike(String value) { 208 | addCriterion("name like", value, "name"); 209 | return (Criteria) this; 210 | } 211 | 212 | public Criteria andNameNotLike(String value) { 213 | addCriterion("name not like", value, "name"); 214 | return (Criteria) this; 215 | } 216 | 217 | public Criteria andNameIn(List values) { 218 | addCriterion("name in", values, "name"); 219 | return (Criteria) this; 220 | } 221 | 222 | public Criteria andNameNotIn(List values) { 223 | addCriterion("name not in", values, "name"); 224 | return (Criteria) this; 225 | } 226 | 227 | public Criteria andNameBetween(String value1, String value2) { 228 | addCriterion("name between", value1, value2, "name"); 229 | return (Criteria) this; 230 | } 231 | 232 | public Criteria andNameNotBetween(String value1, String value2) { 233 | addCriterion("name not between", value1, value2, "name"); 234 | return (Criteria) this; 235 | } 236 | 237 | public Criteria andCountIsNull() { 238 | addCriterion("count is null"); 239 | return (Criteria) this; 240 | } 241 | 242 | public Criteria andCountIsNotNull() { 243 | addCriterion("count is not null"); 244 | return (Criteria) this; 245 | } 246 | 247 | public Criteria andCountEqualTo(Integer value) { 248 | addCriterion("count =", value, "count"); 249 | return (Criteria) this; 250 | } 251 | 252 | public Criteria andCountNotEqualTo(Integer value) { 253 | addCriterion("count <>", value, "count"); 254 | return (Criteria) this; 255 | } 256 | 257 | public Criteria andCountGreaterThan(Integer value) { 258 | addCriterion("count >", value, "count"); 259 | return (Criteria) this; 260 | } 261 | 262 | public Criteria andCountGreaterThanOrEqualTo(Integer value) { 263 | addCriterion("count >=", value, "count"); 264 | return (Criteria) this; 265 | } 266 | 267 | public Criteria andCountLessThan(Integer value) { 268 | addCriterion("count <", value, "count"); 269 | return (Criteria) this; 270 | } 271 | 272 | public Criteria andCountLessThanOrEqualTo(Integer value) { 273 | addCriterion("count <=", value, "count"); 274 | return (Criteria) this; 275 | } 276 | 277 | public Criteria andCountIn(List values) { 278 | addCriterion("count in", values, "count"); 279 | return (Criteria) this; 280 | } 281 | 282 | public Criteria andCountNotIn(List values) { 283 | addCriterion("count not in", values, "count"); 284 | return (Criteria) this; 285 | } 286 | 287 | public Criteria andCountBetween(Integer value1, Integer value2) { 288 | addCriterion("count between", value1, value2, "count"); 289 | return (Criteria) this; 290 | } 291 | 292 | public Criteria andCountNotBetween(Integer value1, Integer value2) { 293 | addCriterion("count not between", value1, value2, "count"); 294 | return (Criteria) this; 295 | } 296 | 297 | public Criteria andSaleIsNull() { 298 | addCriterion("sale is null"); 299 | return (Criteria) this; 300 | } 301 | 302 | public Criteria andSaleIsNotNull() { 303 | addCriterion("sale is not null"); 304 | return (Criteria) this; 305 | } 306 | 307 | public Criteria andSaleEqualTo(Integer value) { 308 | addCriterion("sale =", value, "sale"); 309 | return (Criteria) this; 310 | } 311 | 312 | public Criteria andSaleNotEqualTo(Integer value) { 313 | addCriterion("sale <>", value, "sale"); 314 | return (Criteria) this; 315 | } 316 | 317 | public Criteria andSaleGreaterThan(Integer value) { 318 | addCriterion("sale >", value, "sale"); 319 | return (Criteria) this; 320 | } 321 | 322 | public Criteria andSaleGreaterThanOrEqualTo(Integer value) { 323 | addCriterion("sale >=", value, "sale"); 324 | return (Criteria) this; 325 | } 326 | 327 | public Criteria andSaleLessThan(Integer value) { 328 | addCriterion("sale <", value, "sale"); 329 | return (Criteria) this; 330 | } 331 | 332 | public Criteria andSaleLessThanOrEqualTo(Integer value) { 333 | addCriterion("sale <=", value, "sale"); 334 | return (Criteria) this; 335 | } 336 | 337 | public Criteria andSaleIn(List values) { 338 | addCriterion("sale in", values, "sale"); 339 | return (Criteria) this; 340 | } 341 | 342 | public Criteria andSaleNotIn(List values) { 343 | addCriterion("sale not in", values, "sale"); 344 | return (Criteria) this; 345 | } 346 | 347 | public Criteria andSaleBetween(Integer value1, Integer value2) { 348 | addCriterion("sale between", value1, value2, "sale"); 349 | return (Criteria) this; 350 | } 351 | 352 | public Criteria andSaleNotBetween(Integer value1, Integer value2) { 353 | addCriterion("sale not between", value1, value2, "sale"); 354 | return (Criteria) this; 355 | } 356 | 357 | public Criteria andVersionIsNull() { 358 | addCriterion("version is null"); 359 | return (Criteria) this; 360 | } 361 | 362 | public Criteria andVersionIsNotNull() { 363 | addCriterion("version is not null"); 364 | return (Criteria) this; 365 | } 366 | 367 | public Criteria andVersionEqualTo(Integer value) { 368 | addCriterion("version =", value, "version"); 369 | return (Criteria) this; 370 | } 371 | 372 | public Criteria andVersionNotEqualTo(Integer value) { 373 | addCriterion("version <>", value, "version"); 374 | return (Criteria) this; 375 | } 376 | 377 | public Criteria andVersionGreaterThan(Integer value) { 378 | addCriterion("version >", value, "version"); 379 | return (Criteria) this; 380 | } 381 | 382 | public Criteria andVersionGreaterThanOrEqualTo(Integer value) { 383 | addCriterion("version >=", value, "version"); 384 | return (Criteria) this; 385 | } 386 | 387 | public Criteria andVersionLessThan(Integer value) { 388 | addCriterion("version <", value, "version"); 389 | return (Criteria) this; 390 | } 391 | 392 | public Criteria andVersionLessThanOrEqualTo(Integer value) { 393 | addCriterion("version <=", value, "version"); 394 | return (Criteria) this; 395 | } 396 | 397 | public Criteria andVersionIn(List values) { 398 | addCriterion("version in", values, "version"); 399 | return (Criteria) this; 400 | } 401 | 402 | public Criteria andVersionNotIn(List values) { 403 | addCriterion("version not in", values, "version"); 404 | return (Criteria) this; 405 | } 406 | 407 | public Criteria andVersionBetween(Integer value1, Integer value2) { 408 | addCriterion("version between", value1, value2, "version"); 409 | return (Criteria) this; 410 | } 411 | 412 | public Criteria andVersionNotBetween(Integer value1, Integer value2) { 413 | addCriterion("version not between", value1, value2, "version"); 414 | return (Criteria) this; 415 | } 416 | } 417 | 418 | public static class Criteria extends GeneratedCriteria { 419 | 420 | protected Criteria() { 421 | super(); 422 | } 423 | } 424 | 425 | public static class Criterion { 426 | private String condition; 427 | 428 | private Object value; 429 | 430 | private Object secondValue; 431 | 432 | private boolean noValue; 433 | 434 | private boolean singleValue; 435 | 436 | private boolean betweenValue; 437 | 438 | private boolean listValue; 439 | 440 | private String typeHandler; 441 | 442 | public String getCondition() { 443 | return condition; 444 | } 445 | 446 | public Object getValue() { 447 | return value; 448 | } 449 | 450 | public Object getSecondValue() { 451 | return secondValue; 452 | } 453 | 454 | public boolean isNoValue() { 455 | return noValue; 456 | } 457 | 458 | public boolean isSingleValue() { 459 | return singleValue; 460 | } 461 | 462 | public boolean isBetweenValue() { 463 | return betweenValue; 464 | } 465 | 466 | public boolean isListValue() { 467 | return listValue; 468 | } 469 | 470 | public String getTypeHandler() { 471 | return typeHandler; 472 | } 473 | 474 | protected Criterion(String condition) { 475 | super(); 476 | this.condition = condition; 477 | this.typeHandler = null; 478 | this.noValue = true; 479 | } 480 | 481 | protected Criterion(String condition, Object value, String typeHandler) { 482 | super(); 483 | this.condition = condition; 484 | this.value = value; 485 | this.typeHandler = typeHandler; 486 | if (value instanceof List) { 487 | this.listValue = true; 488 | } else { 489 | this.singleValue = true; 490 | } 491 | } 492 | 493 | protected Criterion(String condition, Object value) { 494 | this(condition, value, null); 495 | } 496 | 497 | protected Criterion(String condition, Object value, Object secondValue, String typeHandler) { 498 | super(); 499 | this.condition = condition; 500 | this.value = value; 501 | this.secondValue = secondValue; 502 | this.typeHandler = typeHandler; 503 | this.betweenValue = true; 504 | } 505 | 506 | protected Criterion(String condition, Object value, Object secondValue) { 507 | this(condition, value, secondValue, null); 508 | } 509 | } 510 | } -------------------------------------------------------------------------------- /miaosha-dao/src/main/java/cn/monitor4all/miaoshadao/dao/StockOrder.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshadao.dao; 2 | 3 | import java.util.Date; 4 | 5 | public class StockOrder { 6 | private Integer id; 7 | 8 | private Integer sid; 9 | 10 | private String name; 11 | 12 | private Integer userId; 13 | 14 | private Date createTime; 15 | 16 | public Integer getId() { 17 | return id; 18 | } 19 | 20 | public void setId(Integer id) { 21 | this.id = id; 22 | } 23 | 24 | public Integer getSid() { 25 | return sid; 26 | } 27 | 28 | public void setSid(Integer sid) { 29 | this.sid = sid; 30 | } 31 | 32 | public String getName() { 33 | return name; 34 | } 35 | 36 | public void setName(String name) { 37 | this.name = name == null ? null : name.trim(); 38 | } 39 | 40 | public Date getCreateTime() { 41 | return createTime; 42 | } 43 | 44 | public void setCreateTime(Date createTime) { 45 | this.createTime = createTime; 46 | } 47 | 48 | public Integer getUserId() { 49 | return userId; 50 | } 51 | 52 | public void setUserId(Integer userId) { 53 | this.userId = userId; 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | StringBuilder sb = new StringBuilder(); 59 | sb.append(getClass().getSimpleName()); 60 | sb.append(" ["); 61 | sb.append("Hash = ").append(hashCode()); 62 | sb.append(", id=").append(id); 63 | sb.append(", sid=").append(sid); 64 | sb.append(", name=").append(name); 65 | sb.append(", createTime=").append(createTime); 66 | sb.append(", userId=").append(userId); 67 | sb.append("]"); 68 | return sb.toString(); 69 | } 70 | } -------------------------------------------------------------------------------- /miaosha-dao/src/main/java/cn/monitor4all/miaoshadao/dao/StockOrderExample.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshadao.dao; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Date; 5 | import java.util.List; 6 | 7 | public class StockOrderExample { 8 | protected String orderByClause; 9 | 10 | protected boolean distinct; 11 | 12 | protected List oredCriteria; 13 | 14 | public StockOrderExample() { 15 | oredCriteria = new ArrayList(); 16 | } 17 | 18 | public void setOrderByClause(String orderByClause) { 19 | this.orderByClause = orderByClause; 20 | } 21 | 22 | public String getOrderByClause() { 23 | return orderByClause; 24 | } 25 | 26 | public void setDistinct(boolean distinct) { 27 | this.distinct = distinct; 28 | } 29 | 30 | public boolean isDistinct() { 31 | return distinct; 32 | } 33 | 34 | public List getOredCriteria() { 35 | return oredCriteria; 36 | } 37 | 38 | public void or(Criteria criteria) { 39 | oredCriteria.add(criteria); 40 | } 41 | 42 | public Criteria or() { 43 | Criteria criteria = createCriteriaInternal(); 44 | oredCriteria.add(criteria); 45 | return criteria; 46 | } 47 | 48 | public Criteria createCriteria() { 49 | Criteria criteria = createCriteriaInternal(); 50 | if (oredCriteria.size() == 0) { 51 | oredCriteria.add(criteria); 52 | } 53 | return criteria; 54 | } 55 | 56 | protected Criteria createCriteriaInternal() { 57 | Criteria criteria = new Criteria(); 58 | return criteria; 59 | } 60 | 61 | public void clear() { 62 | oredCriteria.clear(); 63 | orderByClause = null; 64 | distinct = false; 65 | } 66 | 67 | protected abstract static class GeneratedCriteria { 68 | protected List criteria; 69 | 70 | protected GeneratedCriteria() { 71 | super(); 72 | criteria = new ArrayList(); 73 | } 74 | 75 | public boolean isValid() { 76 | return criteria.size() > 0; 77 | } 78 | 79 | public List getAllCriteria() { 80 | return criteria; 81 | } 82 | 83 | public List getCriteria() { 84 | return criteria; 85 | } 86 | 87 | protected void addCriterion(String condition) { 88 | if (condition == null) { 89 | throw new RuntimeException("Value for condition cannot be null"); 90 | } 91 | criteria.add(new Criterion(condition)); 92 | } 93 | 94 | protected void addCriterion(String condition, Object value, String property) { 95 | if (value == null) { 96 | throw new RuntimeException("Value for " + property + " cannot be null"); 97 | } 98 | criteria.add(new Criterion(condition, value)); 99 | } 100 | 101 | protected void addCriterion(String condition, Object value1, Object value2, String property) { 102 | if (value1 == null || value2 == null) { 103 | throw new RuntimeException("Between values for " + property + " cannot be null"); 104 | } 105 | criteria.add(new Criterion(condition, value1, value2)); 106 | } 107 | 108 | public Criteria andIdIsNull() { 109 | addCriterion("id is null"); 110 | return (Criteria) this; 111 | } 112 | 113 | public Criteria andIdIsNotNull() { 114 | addCriterion("id is not null"); 115 | return (Criteria) this; 116 | } 117 | 118 | public Criteria andIdEqualTo(Integer value) { 119 | addCriterion("id =", value, "id"); 120 | return (Criteria) this; 121 | } 122 | 123 | public Criteria andIdNotEqualTo(Integer value) { 124 | addCriterion("id <>", value, "id"); 125 | return (Criteria) this; 126 | } 127 | 128 | public Criteria andIdGreaterThan(Integer value) { 129 | addCriterion("id >", value, "id"); 130 | return (Criteria) this; 131 | } 132 | 133 | public Criteria andIdGreaterThanOrEqualTo(Integer value) { 134 | addCriterion("id >=", value, "id"); 135 | return (Criteria) this; 136 | } 137 | 138 | public Criteria andIdLessThan(Integer value) { 139 | addCriterion("id <", value, "id"); 140 | return (Criteria) this; 141 | } 142 | 143 | public Criteria andIdLessThanOrEqualTo(Integer value) { 144 | addCriterion("id <=", value, "id"); 145 | return (Criteria) this; 146 | } 147 | 148 | public Criteria andIdIn(List values) { 149 | addCriterion("id in", values, "id"); 150 | return (Criteria) this; 151 | } 152 | 153 | public Criteria andIdNotIn(List values) { 154 | addCriterion("id not in", values, "id"); 155 | return (Criteria) this; 156 | } 157 | 158 | public Criteria andIdBetween(Integer value1, Integer value2) { 159 | addCriterion("id between", value1, value2, "id"); 160 | return (Criteria) this; 161 | } 162 | 163 | public Criteria andIdNotBetween(Integer value1, Integer value2) { 164 | addCriterion("id not between", value1, value2, "id"); 165 | return (Criteria) this; 166 | } 167 | 168 | public Criteria andSidIsNull() { 169 | addCriterion("sid is null"); 170 | return (Criteria) this; 171 | } 172 | 173 | public Criteria andSidIsNotNull() { 174 | addCriterion("sid is not null"); 175 | return (Criteria) this; 176 | } 177 | 178 | public Criteria andSidEqualTo(Integer value) { 179 | addCriterion("sid =", value, "sid"); 180 | return (Criteria) this; 181 | } 182 | 183 | public Criteria andSidNotEqualTo(Integer value) { 184 | addCriterion("sid <>", value, "sid"); 185 | return (Criteria) this; 186 | } 187 | 188 | public Criteria andSidGreaterThan(Integer value) { 189 | addCriterion("sid >", value, "sid"); 190 | return (Criteria) this; 191 | } 192 | 193 | public Criteria andSidGreaterThanOrEqualTo(Integer value) { 194 | addCriterion("sid >=", value, "sid"); 195 | return (Criteria) this; 196 | } 197 | 198 | public Criteria andSidLessThan(Integer value) { 199 | addCriterion("sid <", value, "sid"); 200 | return (Criteria) this; 201 | } 202 | 203 | public Criteria andSidLessThanOrEqualTo(Integer value) { 204 | addCriterion("sid <=", value, "sid"); 205 | return (Criteria) this; 206 | } 207 | 208 | public Criteria andSidIn(List values) { 209 | addCriterion("sid in", values, "sid"); 210 | return (Criteria) this; 211 | } 212 | 213 | public Criteria andSidNotIn(List values) { 214 | addCriterion("sid not in", values, "sid"); 215 | return (Criteria) this; 216 | } 217 | 218 | public Criteria andSidBetween(Integer value1, Integer value2) { 219 | addCriterion("sid between", value1, value2, "sid"); 220 | return (Criteria) this; 221 | } 222 | 223 | public Criteria andSidNotBetween(Integer value1, Integer value2) { 224 | addCriterion("sid not between", value1, value2, "sid"); 225 | return (Criteria) this; 226 | } 227 | 228 | public Criteria andNameIsNull() { 229 | addCriterion("name is null"); 230 | return (Criteria) this; 231 | } 232 | 233 | public Criteria andNameIsNotNull() { 234 | addCriterion("name is not null"); 235 | return (Criteria) this; 236 | } 237 | 238 | public Criteria andNameEqualTo(String value) { 239 | addCriterion("name =", value, "name"); 240 | return (Criteria) this; 241 | } 242 | 243 | public Criteria andNameNotEqualTo(String value) { 244 | addCriterion("name <>", value, "name"); 245 | return (Criteria) this; 246 | } 247 | 248 | public Criteria andNameGreaterThan(String value) { 249 | addCriterion("name >", value, "name"); 250 | return (Criteria) this; 251 | } 252 | 253 | public Criteria andNameGreaterThanOrEqualTo(String value) { 254 | addCriterion("name >=", value, "name"); 255 | return (Criteria) this; 256 | } 257 | 258 | public Criteria andNameLessThan(String value) { 259 | addCriterion("name <", value, "name"); 260 | return (Criteria) this; 261 | } 262 | 263 | public Criteria andNameLessThanOrEqualTo(String value) { 264 | addCriterion("name <=", value, "name"); 265 | return (Criteria) this; 266 | } 267 | 268 | public Criteria andNameLike(String value) { 269 | addCriterion("name like", value, "name"); 270 | return (Criteria) this; 271 | } 272 | 273 | public Criteria andNameNotLike(String value) { 274 | addCriterion("name not like", value, "name"); 275 | return (Criteria) this; 276 | } 277 | 278 | public Criteria andNameIn(List values) { 279 | addCriterion("name in", values, "name"); 280 | return (Criteria) this; 281 | } 282 | 283 | public Criteria andNameNotIn(List values) { 284 | addCriterion("name not in", values, "name"); 285 | return (Criteria) this; 286 | } 287 | 288 | public Criteria andNameBetween(String value1, String value2) { 289 | addCriterion("name between", value1, value2, "name"); 290 | return (Criteria) this; 291 | } 292 | 293 | public Criteria andNameNotBetween(String value1, String value2) { 294 | addCriterion("name not between", value1, value2, "name"); 295 | return (Criteria) this; 296 | } 297 | 298 | public Criteria andCreateTimeIsNull() { 299 | addCriterion("create_time is null"); 300 | return (Criteria) this; 301 | } 302 | 303 | public Criteria andCreateTimeIsNotNull() { 304 | addCriterion("create_time is not null"); 305 | return (Criteria) this; 306 | } 307 | 308 | public Criteria andCreateTimeEqualTo(Date value) { 309 | addCriterion("create_time =", value, "createTime"); 310 | return (Criteria) this; 311 | } 312 | 313 | public Criteria andCreateTimeNotEqualTo(Date value) { 314 | addCriterion("create_time <>", value, "createTime"); 315 | return (Criteria) this; 316 | } 317 | 318 | public Criteria andCreateTimeGreaterThan(Date value) { 319 | addCriterion("create_time >", value, "createTime"); 320 | return (Criteria) this; 321 | } 322 | 323 | public Criteria andCreateTimeGreaterThanOrEqualTo(Date value) { 324 | addCriterion("create_time >=", value, "createTime"); 325 | return (Criteria) this; 326 | } 327 | 328 | public Criteria andCreateTimeLessThan(Date value) { 329 | addCriterion("create_time <", value, "createTime"); 330 | return (Criteria) this; 331 | } 332 | 333 | public Criteria andCreateTimeLessThanOrEqualTo(Date value) { 334 | addCriterion("create_time <=", value, "createTime"); 335 | return (Criteria) this; 336 | } 337 | 338 | public Criteria andCreateTimeIn(List values) { 339 | addCriterion("create_time in", values, "createTime"); 340 | return (Criteria) this; 341 | } 342 | 343 | public Criteria andCreateTimeNotIn(List values) { 344 | addCriterion("create_time not in", values, "createTime"); 345 | return (Criteria) this; 346 | } 347 | 348 | public Criteria andCreateTimeBetween(Date value1, Date value2) { 349 | addCriterion("create_time between", value1, value2, "createTime"); 350 | return (Criteria) this; 351 | } 352 | 353 | public Criteria andCreateTimeNotBetween(Date value1, Date value2) { 354 | addCriterion("create_time not between", value1, value2, "createTime"); 355 | return (Criteria) this; 356 | } 357 | } 358 | 359 | public static class Criteria extends GeneratedCriteria { 360 | 361 | protected Criteria() { 362 | super(); 363 | } 364 | } 365 | 366 | public static class Criterion { 367 | private String condition; 368 | 369 | private Object value; 370 | 371 | private Object secondValue; 372 | 373 | private boolean noValue; 374 | 375 | private boolean singleValue; 376 | 377 | private boolean betweenValue; 378 | 379 | private boolean listValue; 380 | 381 | private String typeHandler; 382 | 383 | public String getCondition() { 384 | return condition; 385 | } 386 | 387 | public Object getValue() { 388 | return value; 389 | } 390 | 391 | public Object getSecondValue() { 392 | return secondValue; 393 | } 394 | 395 | public boolean isNoValue() { 396 | return noValue; 397 | } 398 | 399 | public boolean isSingleValue() { 400 | return singleValue; 401 | } 402 | 403 | public boolean isBetweenValue() { 404 | return betweenValue; 405 | } 406 | 407 | public boolean isListValue() { 408 | return listValue; 409 | } 410 | 411 | public String getTypeHandler() { 412 | return typeHandler; 413 | } 414 | 415 | protected Criterion(String condition) { 416 | super(); 417 | this.condition = condition; 418 | this.typeHandler = null; 419 | this.noValue = true; 420 | } 421 | 422 | protected Criterion(String condition, Object value, String typeHandler) { 423 | super(); 424 | this.condition = condition; 425 | this.value = value; 426 | this.typeHandler = typeHandler; 427 | if (value instanceof List) { 428 | this.listValue = true; 429 | } else { 430 | this.singleValue = true; 431 | } 432 | } 433 | 434 | protected Criterion(String condition, Object value) { 435 | this(condition, value, null); 436 | } 437 | 438 | protected Criterion(String condition, Object value, Object secondValue, String typeHandler) { 439 | super(); 440 | this.condition = condition; 441 | this.value = value; 442 | this.secondValue = secondValue; 443 | this.typeHandler = typeHandler; 444 | this.betweenValue = true; 445 | } 446 | 447 | protected Criterion(String condition, Object value, Object secondValue) { 448 | this(condition, value, secondValue, null); 449 | } 450 | } 451 | } -------------------------------------------------------------------------------- /miaosha-dao/src/main/java/cn/monitor4all/miaoshadao/dao/User.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshadao.dao; 2 | 3 | public class User { 4 | private Long id; 5 | 6 | private String userName; 7 | 8 | public User(Long id, String userName) { 9 | this.id = id; 10 | this.userName = userName; 11 | } 12 | 13 | public User() { 14 | super(); 15 | } 16 | 17 | public Long getId() { 18 | return id; 19 | } 20 | 21 | public void setId(Long id) { 22 | this.id = id; 23 | } 24 | 25 | public String getUserName() { 26 | return userName; 27 | } 28 | 29 | public void setUserName(String userName) { 30 | this.userName = userName == null ? null : userName.trim(); 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | StringBuilder sb = new StringBuilder(); 36 | sb.append(getClass().getSimpleName()); 37 | sb.append(" ["); 38 | sb.append("Hash = ").append(hashCode()); 39 | sb.append(", id=").append(id); 40 | sb.append(", userName=").append(userName); 41 | sb.append("]"); 42 | return sb.toString(); 43 | } 44 | } -------------------------------------------------------------------------------- /miaosha-dao/src/main/java/cn/monitor4all/miaoshadao/mapper/StockMapper.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshadao.mapper; 2 | 3 | import cn.monitor4all.miaoshadao.dao.Stock; 4 | import org.apache.ibatis.annotations.Mapper; 5 | 6 | @Mapper 7 | public interface StockMapper { 8 | 9 | int deleteByPrimaryKey(Integer id); 10 | 11 | int insert(Stock record); 12 | 13 | int insertSelective(Stock record); 14 | 15 | Stock selectByPrimaryKey(Integer id); 16 | 17 | Stock selectByPrimaryKeyForUpdate(Integer id); 18 | 19 | int updateByPrimaryKeySelective(Stock record); 20 | 21 | int updateByPrimaryKey(Stock record); 22 | 23 | int updateByOptimistic(Stock record); 24 | } -------------------------------------------------------------------------------- /miaosha-dao/src/main/java/cn/monitor4all/miaoshadao/mapper/StockOrderMapper.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshadao.mapper; 2 | 3 | import java.util.List; 4 | 5 | import cn.monitor4all.miaoshadao.dao.StockOrder; 6 | import org.apache.ibatis.annotations.Mapper; 7 | import org.apache.ibatis.annotations.Param; 8 | 9 | @Mapper 10 | public interface StockOrderMapper { 11 | 12 | int deleteByPrimaryKey(Integer id); 13 | 14 | int insert(StockOrder record); 15 | 16 | int insertSelective(StockOrder record); 17 | 18 | StockOrder selectByPrimaryKey(Integer id); 19 | 20 | int updateByPrimaryKeySelective(StockOrder record); 21 | 22 | int updateByPrimaryKey(StockOrder record); 23 | } -------------------------------------------------------------------------------- /miaosha-dao/src/main/java/cn/monitor4all/miaoshadao/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshadao.mapper; 2 | 3 | import cn.monitor4all.miaoshadao.dao.User; 4 | 5 | public interface UserMapper { 6 | int deleteByPrimaryKey(Long id); 7 | 8 | int insert(User record); 9 | 10 | int insertSelective(User record); 11 | 12 | User selectByPrimaryKey(Long id); 13 | 14 | int updateByPrimaryKeySelective(User record); 15 | 16 | int updateByPrimaryKey(User record); 17 | } -------------------------------------------------------------------------------- /miaosha-dao/src/main/java/cn/monitor4all/miaoshadao/utils/CacheKey.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshadao.utils; 2 | 3 | public enum CacheKey { 4 | 5 | HASH_KEY("miaosha_v1_user_hash"), 6 | LIMIT_KEY("miaosha_v1_user_limit"), 7 | STOCK_COUNT("miaosha_v1_stock_count"), 8 | USER_HAS_ORDER("miaosha_v1_user_has_order"); 9 | 10 | private String key; 11 | private CacheKey(String key) { 12 | this.key = key; 13 | } 14 | public String getKey() { 15 | return key; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /miaosha-dao/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.driver-class-name = com.mysql.jdbc.Driver 2 | spring.datasource.url = jdbc:mysql://localhost:3306/m4a_miaosha?characterEncoding=utf-8 3 | spring.datasource.username = root 4 | spring.datasource.password = root 5 | mybatis.mapper-locations=classpath:/mapper/*.xml -------------------------------------------------------------------------------- /miaosha-dao/src/main/resources/generatorConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /miaosha-dao/src/main/resources/mapper/StockMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | id, name, count, sale, version 13 | 14 | 20 | 27 | 28 | delete from stock 29 | where id = #{id,jdbcType=INTEGER} 30 | 31 | 32 | insert into stock (id, name, count, 33 | sale, version) 34 | values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, #{count,jdbcType=INTEGER}, 35 | #{sale,jdbcType=INTEGER}, #{version,jdbcType=INTEGER}) 36 | 37 | 38 | insert into stock 39 | 40 | 41 | id, 42 | 43 | 44 | name, 45 | 46 | 47 | count, 48 | 49 | 50 | sale, 51 | 52 | 53 | version, 54 | 55 | 56 | 57 | 58 | #{id,jdbcType=INTEGER}, 59 | 60 | 61 | #{name,jdbcType=VARCHAR}, 62 | 63 | 64 | #{count,jdbcType=INTEGER}, 65 | 66 | 67 | #{sale,jdbcType=INTEGER}, 68 | 69 | 70 | #{version,jdbcType=INTEGER}, 71 | 72 | 73 | 74 | 75 | update stock 76 | 77 | 78 | name = #{name,jdbcType=VARCHAR}, 79 | 80 | 81 | count = #{count,jdbcType=INTEGER}, 82 | 83 | 84 | sale = #{sale,jdbcType=INTEGER}, 85 | 86 | 87 | version = #{version,jdbcType=INTEGER}, 88 | 89 | 90 | where id = #{id,jdbcType=INTEGER} 91 | 92 | 93 | update stock 94 | set name = #{name,jdbcType=VARCHAR}, 95 | count = #{count,jdbcType=INTEGER}, 96 | sale = #{sale,jdbcType=INTEGER}, 97 | version = #{version,jdbcType=INTEGER} 98 | where id = #{id,jdbcType=INTEGER} 99 | 100 | 101 | 102 | update stock 103 | 104 | sale = sale + 1, 105 | 106 | WHERE id = #{id,jdbcType=INTEGER} 107 | AND sale = #{sale,jdbcType=INTEGER} 108 | 109 | -------------------------------------------------------------------------------- /miaosha-dao/src/main/resources/mapper/StockOrderMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | id, sid, name, user_id, create_time 14 | 15 | 16 | 22 | 23 | delete from stock_order 24 | where id = #{id,jdbcType=INTEGER} 25 | 26 | 27 | insert into stock_order (id, sid, name, user_id, create_time) 28 | values (#{id,jdbcType=INTEGER}, #{sid,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, 29 | #{createTime,jdbcType=TIMESTAMP}) 30 | 31 | 32 | insert into stock_order 33 | 34 | 35 | id, 36 | 37 | 38 | sid, 39 | 40 | 41 | name, 42 | 43 | 44 | user_id, 45 | 46 | 47 | create_time, 48 | 49 | 50 | 51 | 52 | #{id,jdbcType=INTEGER}, 53 | 54 | 55 | #{sid,jdbcType=INTEGER}, 56 | 57 | 58 | #{name,jdbcType=VARCHAR}, 59 | 60 | 61 | #{userId,jdbcType=INTEGER}, 62 | 63 | 64 | #{createTime,jdbcType=TIMESTAMP}, 65 | 66 | 67 | 68 | 69 | update stock_order 70 | 71 | 72 | sid = #{sid,jdbcType=INTEGER}, 73 | 74 | 75 | name = #{name,jdbcType=VARCHAR}, 76 | 77 | 78 | user_id = #{userId,jdbcType=INTEGER}, 79 | 80 | 81 | create_time = #{createTime,jdbcType=TIMESTAMP}, 82 | 83 | 84 | where id = #{id,jdbcType=INTEGER} 85 | 86 | 87 | update stock_order 88 | set sid = #{sid,jdbcType=INTEGER}, 89 | name = #{name,jdbcType=VARCHAR}, 90 | user_id = #{userId,jdbcType=INTEGER}, 91 | create_time = #{createTime,jdbcType=TIMESTAMP} 92 | where id = #{id,jdbcType=INTEGER} 93 | 94 | -------------------------------------------------------------------------------- /miaosha-dao/src/main/resources/mapper/UserMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | id, user_name 12 | 13 | 19 | 20 | delete from user 21 | where id = #{id,jdbcType=BIGINT} 22 | 23 | 24 | insert into user (id, user_name) 25 | values (#{id,jdbcType=BIGINT}, #{userName,jdbcType=VARCHAR}) 26 | 27 | 28 | insert into user 29 | 30 | 31 | id, 32 | 33 | 34 | user_name, 35 | 36 | 37 | 38 | 39 | #{id,jdbcType=BIGINT}, 40 | 41 | 42 | #{userName,jdbcType=VARCHAR}, 43 | 44 | 45 | 46 | 47 | update user 48 | 49 | 50 | user_name = #{userName,jdbcType=VARCHAR}, 51 | 52 | 53 | where id = #{id,jdbcType=BIGINT} 54 | 55 | 56 | update user 57 | set user_name = #{userName,jdbcType=VARCHAR} 58 | where id = #{id,jdbcType=BIGINT} 59 | 60 | -------------------------------------------------------------------------------- /miaosha-dao/src/test/java/cn/monitor4all/miaoshadao/MiaoshaDaoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshadao; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class MiaoshaDaoApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /miaosha-job/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | miaosha 7 | cn.monitor4all 8 | 1.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | miaosha-job 13 | 14 | 15 | 1.8 16 | 17 | 18 | 19 | 20 | 21 | com.alibaba.otter 22 | canal.client 23 | 1.1.0 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /miaosha-job/src/main/java/job/CanalClient.java: -------------------------------------------------------------------------------- 1 | package job; 2 | 3 | import com.alibaba.otter.canal.client.CanalConnector; 4 | import com.alibaba.otter.canal.client.CanalConnectors; 5 | import com.alibaba.otter.canal.protocol.CanalEntry.*; 6 | import com.alibaba.otter.canal.protocol.Message; 7 | import com.google.protobuf.InvalidProtocolBufferException; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.net.InetSocketAddress; 12 | import java.util.List; 13 | import java.util.concurrent.atomic.AtomicInteger; 14 | 15 | public class CanalClient { 16 | 17 | private static final Logger LOGGER = LoggerFactory.getLogger(CanalClient.class); 18 | 19 | public static void main(String[] args) { 20 | 21 | // 第一步:与canal进行连接 22 | CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("127.0.0.1", 11111), 23 | "example", "", ""); 24 | connector.connect(); 25 | 26 | // 第二步:开启订阅 27 | connector.subscribe(); 28 | 29 | // 第三步:循环订阅 30 | while (true) { 31 | try { 32 | // 每次读取 1000 条 33 | Message message = connector.getWithoutAck(1000); 34 | 35 | long batchID = message.getId(); 36 | 37 | int size = message.getEntries().size(); 38 | 39 | if (batchID == -1 || size == 0) { 40 | LOGGER.info("当前暂时没有数据,休眠1秒"); 41 | Thread.sleep(1000); 42 | } else { 43 | LOGGER.info("-------------------------- 有数据啦 -----------------------"); 44 | printEntry(message.getEntries()); 45 | } 46 | 47 | connector.ack(batchID); 48 | 49 | } catch (Exception e) { 50 | LOGGER.error("处理出错"); 51 | } finally { 52 | try { 53 | Thread.sleep(1000); 54 | } catch (InterruptedException e) { 55 | e.printStackTrace(); 56 | } 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * 获取每条打印的记录 63 | */ 64 | public static void printEntry(List entrys) { 65 | 66 | for (Entry entry : entrys) { 67 | 68 | // 第一步:拆解entry 实体 69 | Header header = entry.getHeader(); 70 | EntryType entryType = entry.getEntryType(); 71 | 72 | // 第二步: 如果当前是RowData,那就是我需要的数据 73 | if (entryType == EntryType.ROWDATA) { 74 | 75 | String tableName = header.getTableName(); 76 | String schemaName = header.getSchemaName(); 77 | 78 | RowChange rowChange = null; 79 | 80 | try { 81 | rowChange = RowChange.parseFrom(entry.getStoreValue()); 82 | } catch (InvalidProtocolBufferException e) { 83 | e.printStackTrace(); 84 | } 85 | 86 | EventType eventType = rowChange.getEventType(); 87 | 88 | LOGGER.info(String.format("当前正在操作表 %s.%s, 执行操作= %s", schemaName, tableName, eventType)); 89 | 90 | // 如果是‘查询’ 或者 是 ‘DDL’ 操作,那么sql直接打出来 91 | if (eventType == EventType.QUERY || rowChange.getIsDdl()) { 92 | LOGGER.info("执行了查询语句:[{}]", rowChange.getSql()); 93 | return; 94 | } 95 | 96 | // 第三步:追踪到 columns 级别 97 | rowChange.getRowDatasList().forEach((rowData) -> { 98 | 99 | // 获取更新之前的column情况 100 | List beforeColumns = rowData.getBeforeColumnsList(); 101 | 102 | // 获取更新之后的 column 情况 103 | List afterColumns = rowData.getAfterColumnsList(); 104 | 105 | // 当前执行的是 删除操作 106 | if (eventType == EventType.DELETE) { 107 | printColumn(beforeColumns); 108 | } 109 | 110 | // 当前执行的是 插入操作 111 | if (eventType == EventType.INSERT) { 112 | printColumn(afterColumns); 113 | } 114 | 115 | // 当前执行的是 更新操作 116 | if (eventType == EventType.UPDATE) { 117 | printColumn(afterColumns); 118 | // 进行删除缓存操作 119 | deleteCache(afterColumns, tableName, schemaName); 120 | } 121 | 122 | 123 | }); 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * 每个row上面的每一个column 的更改情况 130 | * @param columns 131 | */ 132 | public static void printColumn(List columns) { 133 | 134 | columns.forEach((column) -> { 135 | String columnName = column.getName(); 136 | String columnValue = column.getValue(); 137 | String columnType = column.getMysqlType(); 138 | // 判断 该字段是否更新 139 | boolean isUpdated = column.getUpdated(); 140 | LOGGER.info(String.format("数据列:columnName=%s, columnValue=%s, columnType=%s, isUpdated=%s", columnName, columnValue, columnType, isUpdated)); 141 | }); 142 | } 143 | 144 | /** 145 | * 秒杀下单接口删除库存缓存 146 | */ 147 | public static void deleteCache(List columns, String tableName, String schemaName) { 148 | if ("stock".equals(tableName) && "m4a_miaosha".equals(schemaName)) { 149 | AtomicInteger id = new AtomicInteger(); 150 | columns.forEach((column) -> { 151 | String columnName = column.getName(); 152 | String columnValue = column.getValue(); 153 | if ("id".equals(columnName)) { 154 | id.set(Integer.parseInt(columnValue)); 155 | } 156 | }); 157 | // TODO: 删除缓存 158 | LOGGER.info("Canal删除stock表id:[{}] 的库存缓存", id); 159 | 160 | } 161 | } 162 | 163 | 164 | } 165 | -------------------------------------------------------------------------------- /miaosha-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /miaosha-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | cn.monitor4all 7 | miaosha 8 | 1.0.0-SNAPSHOT 9 | 10 | miaosha-service 11 | 12 | 13 | 1.8 14 | 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-test 25 | test 26 | 27 | 28 | org.junit.vintage 29 | junit-vintage-engine 30 | 31 | 32 | 33 | 34 | 35 | org.mybatis.spring.boot 36 | mybatis-spring-boot-starter 37 | 1.2.0 38 | 39 | 40 | 41 | mysql 42 | mysql-connector-java 43 | 5.1.6 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-data-redis 49 | 50 | 51 | 52 | cn.monitor4all 53 | miaosha-dao 54 | 1.0.0-SNAPSHOT 55 | 56 | 57 | 58 | 59 | 60 | 61 | maven-compiler-plugin 62 | 63 | 1.8 64 | 1.8 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /miaosha-service/src/main/java/cn/monitor4all/miaoshaservice/MiaoshaServiceApplication.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaservice; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | @MapperScan("cn.monitor4all.miaoshadao.mapper") 9 | public class MiaoshaServiceApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(MiaoshaServiceApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /miaosha-service/src/main/java/cn/monitor4all/miaoshaservice/service/OrderService.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaservice.service; 2 | 3 | public interface OrderService { 4 | 5 | /** 6 | * 创建错误订单 7 | * @param sid 8 | * 库存ID 9 | * @return 10 | * 订单ID 11 | */ 12 | public int createWrongOrder(int sid); 13 | 14 | 15 | /** 16 | * 创建正确订单:下单乐观锁 17 | * @param sid 18 | * @return 19 | * @throws Exception 20 | */ 21 | public int createOptimisticOrder(int sid); 22 | 23 | /** 24 | * 创建正确订单:下单悲观锁 for update 25 | * @param sid 26 | * @return 27 | * @throws Exception 28 | */ 29 | public int createPessimisticOrder(int sid); 30 | 31 | /** 32 | * 创建正确订单:验证库存 + 用户 + 时间 合法性 + 下单乐观锁 33 | * @param sid 34 | * @param userId 35 | * @param verifyHash 36 | * @return 37 | * @throws Exception 38 | */ 39 | public int createVerifiedOrder(Integer sid, Integer userId, String verifyHash) throws Exception; 40 | 41 | /** 42 | * 创建正确订单:验证库存 + 下单乐观锁 + 更新订单信息到缓存 43 | * @param sid 44 | * @param userId 45 | * @throws Exception 46 | */ 47 | public void createOrderByMq(Integer sid, Integer userId) throws Exception; 48 | 49 | /** 50 | * 检查缓存中用户是否已经有订单 51 | * @param sid 52 | * @param userId 53 | * @return 54 | * @throws Exception 55 | */ 56 | public Boolean checkUserOrderInfoInCache(Integer sid, Integer userId) throws Exception; 57 | 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /miaosha-service/src/main/java/cn/monitor4all/miaoshaservice/service/OrderServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaservice.service; 2 | 3 | import cn.monitor4all.miaoshadao.dao.Stock; 4 | import cn.monitor4all.miaoshadao.dao.StockOrder; 5 | import cn.monitor4all.miaoshadao.dao.User; 6 | import cn.monitor4all.miaoshadao.mapper.StockOrderMapper; 7 | import cn.monitor4all.miaoshadao.mapper.UserMapper; 8 | import cn.monitor4all.miaoshadao.utils.CacheKey; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | import org.springframework.data.redis.core.StringRedisTemplate; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Propagation; 16 | import org.springframework.transaction.annotation.Transactional; 17 | 18 | import java.util.Arrays; 19 | import java.util.Collections; 20 | 21 | @Service 22 | public class OrderServiceImpl implements OrderService { 23 | 24 | private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceImpl.class); 25 | 26 | @Autowired 27 | private StringRedisTemplate stringRedisTemplate; 28 | 29 | @Autowired 30 | private StockService stockService; 31 | 32 | @Autowired 33 | private StockOrderMapper orderMapper; 34 | 35 | @Autowired 36 | private UserMapper userMapper; 37 | 38 | @Override 39 | public int createWrongOrder(int sid) { 40 | //校验库存 41 | Stock stock = checkStock(sid); 42 | //扣库存 43 | saleStock(stock); 44 | //创建订单 45 | int id = createOrder(stock); 46 | return id; 47 | } 48 | 49 | @Override 50 | public int createOptimisticOrder(int sid) { 51 | //校验库存 52 | Stock stock = checkStock(sid); 53 | //乐观锁更新库存 54 | boolean success = saleStockOptimistic(stock); 55 | if (!success){ 56 | throw new RuntimeException("过期库存值,更新失败"); 57 | } 58 | //创建订单 59 | createOrder(stock); 60 | return stock.getCount() - (stock.getSale()+1); 61 | } 62 | 63 | @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) 64 | @Override 65 | public int createPessimisticOrder(int sid){ 66 | //校验库存(悲观锁for update) 67 | Stock stock = checkStockForUpdate(sid); 68 | //更新库存 69 | saleStock(stock); 70 | //创建订单 71 | createOrder(stock); 72 | return stock.getCount() - (stock.getSale()); 73 | } 74 | 75 | @Override 76 | public int createVerifiedOrder(Integer sid, Integer userId, String verifyHash) throws Exception { 77 | 78 | // 验证是否在抢购时间内 79 | LOGGER.info("请自行验证是否在抢购时间内,假设此处验证成功"); 80 | 81 | // 验证hash值合法性 82 | String hashKey = CacheKey.HASH_KEY.getKey() + "_" + sid + "_" + userId; 83 | System.out.println(hashKey); 84 | String verifyHashInRedis = stringRedisTemplate.opsForValue().get(hashKey); 85 | if (!verifyHash.equals(verifyHashInRedis)) { 86 | throw new Exception("hash值与Redis中不符合"); 87 | } 88 | LOGGER.info("验证hash值合法性成功"); 89 | 90 | // 检查用户合法性 91 | User user = userMapper.selectByPrimaryKey(userId.longValue()); 92 | if (user == null) { 93 | throw new Exception("用户不存在"); 94 | } 95 | LOGGER.info("用户信息验证成功:[{}]", user.toString()); 96 | 97 | // 检查商品合法性 98 | Stock stock = stockService.getStockById(sid); 99 | if (stock == null) { 100 | throw new Exception("商品不存在"); 101 | } 102 | LOGGER.info("商品信息验证成功:[{}]", stock.toString()); 103 | 104 | //乐观锁更新库存 105 | boolean success = saleStockOptimistic(stock); 106 | if (!success){ 107 | throw new RuntimeException("过期库存值,更新失败"); 108 | } 109 | LOGGER.info("乐观锁更新库存成功"); 110 | 111 | //创建订单 112 | createOrderWithUserInfoInDB(stock, userId); 113 | LOGGER.info("创建订单成功"); 114 | 115 | return stock.getCount() - (stock.getSale()+1); 116 | } 117 | 118 | @Override 119 | public void createOrderByMq(Integer sid, Integer userId) throws Exception { 120 | 121 | // 模拟多个用户同时抢购,导致消息队列排队等候10秒 122 | Thread.sleep(10000); 123 | 124 | Stock stock; 125 | //校验库存(不要学我在trycatch中做逻辑处理,这样是不优雅的。这里这样处理是为了兼容之前的秒杀系统文章) 126 | try { 127 | stock = checkStock(sid); 128 | } catch (Exception e) { 129 | LOGGER.info("库存不足!"); 130 | return; 131 | } 132 | //乐观锁更新库存 133 | boolean updateStock = saleStockOptimistic(stock); 134 | if (!updateStock) { 135 | LOGGER.warn("扣减库存失败,库存已经为0"); 136 | return; 137 | } 138 | 139 | LOGGER.info("扣减库存成功,剩余库存:[{}]", stock.getCount() - stock.getSale() - 1); 140 | stockService.delStockCountCache(sid); 141 | LOGGER.info("删除库存缓存"); 142 | 143 | //创建订单 144 | LOGGER.info("写入订单至数据库"); 145 | createOrderWithUserInfoInDB(stock, userId); 146 | LOGGER.info("写入订单至缓存供查询"); 147 | createOrderWithUserInfoInCache(stock, userId); 148 | LOGGER.info("下单完成"); 149 | 150 | } 151 | 152 | @Override 153 | public Boolean checkUserOrderInfoInCache(Integer sid, Integer userId) throws Exception { 154 | String key = CacheKey.USER_HAS_ORDER.getKey() + "_" + sid; 155 | LOGGER.info("检查用户Id:[{}] 是否抢购过商品Id:[{}] 检查Key:[{}]", userId, sid, key); 156 | return stringRedisTemplate.opsForSet().isMember(key, userId.toString()); 157 | } 158 | 159 | /** 160 | * 检查库存 161 | * @param sid 162 | * @return 163 | */ 164 | private Stock checkStock(int sid) { 165 | Stock stock = stockService.getStockById(sid); 166 | if (stock.getSale().equals(stock.getCount())) { 167 | throw new RuntimeException("库存不足"); 168 | } 169 | return stock; 170 | } 171 | 172 | /** 173 | * 检查库存 ForUpdate 174 | * @param sid 175 | * @return 176 | */ 177 | private Stock checkStockForUpdate(int sid) { 178 | Stock stock = stockService.getStockByIdForUpdate(sid); 179 | if (stock.getSale().equals(stock.getCount())) { 180 | throw new RuntimeException("库存不足"); 181 | } 182 | return stock; 183 | } 184 | 185 | /** 186 | * 更新库存 187 | * @param stock 188 | */ 189 | private void saleStock(Stock stock) { 190 | stock.setSale(stock.getSale() + 1); 191 | stockService.updateStockById(stock); 192 | } 193 | 194 | /** 195 | * 更新库存 乐观锁 196 | * @param stock 197 | */ 198 | private boolean saleStockOptimistic(Stock stock) { 199 | LOGGER.info("查询数据库,尝试更新库存"); 200 | int count = stockService.updateStockByOptimistic(stock); 201 | return count != 0; 202 | } 203 | 204 | /** 205 | * 创建订单 206 | * @param stock 207 | * @return 208 | */ 209 | private int createOrder(Stock stock) { 210 | StockOrder order = new StockOrder(); 211 | order.setSid(stock.getId()); 212 | order.setName(stock.getName()); 213 | return orderMapper.insertSelective(order); 214 | } 215 | 216 | /** 217 | * 创建订单:保存用户订单信息到数据库 218 | * @param stock 219 | * @return 220 | */ 221 | private int createOrderWithUserInfoInDB(Stock stock, Integer userId) { 222 | StockOrder order = new StockOrder(); 223 | order.setSid(stock.getId()); 224 | order.setName(stock.getName()); 225 | order.setUserId(userId); 226 | return orderMapper.insertSelective(order); 227 | } 228 | 229 | /** 230 | * 创建订单:保存用户订单信息到缓存 231 | * @param stock 232 | * @return 返回添加的个数 233 | */ 234 | private Long createOrderWithUserInfoInCache(Stock stock, Integer userId) { 235 | String key = CacheKey.USER_HAS_ORDER.getKey() + "_" + stock.getId().toString(); 236 | LOGGER.info("写入用户订单数据Set:[{}] [{}]", key, userId.toString()); 237 | return stringRedisTemplate.opsForSet().add(key, userId.toString()); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /miaosha-service/src/main/java/cn/monitor4all/miaoshaservice/service/StockService.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaservice.service; 2 | 3 | import cn.monitor4all.miaoshadao.dao.Stock; 4 | 5 | public interface StockService { 6 | 7 | /** 8 | * 查询库存:通过缓存查询库存 9 | * 缓存命中:返回库存 10 | * 缓存未命中:查询数据库写入缓存并返回 11 | * @param id 12 | * @return 13 | */ 14 | Integer getStockCount(int id); 15 | 16 | /** 17 | * 获取剩余库存:查数据库 18 | * @param id 19 | * @return 20 | */ 21 | int getStockCountByDB(int id); 22 | 23 | /** 24 | * 获取剩余库存: 查缓存 25 | * @param id 26 | * @return 27 | */ 28 | Integer getStockCountByCache(int id); 29 | 30 | /** 31 | * 将库存插入缓存 32 | * @param id 33 | * @return 34 | */ 35 | void setStockCountCache(int id, int count); 36 | 37 | /** 38 | * 删除库存缓存 39 | * @param id 40 | */ 41 | void delStockCountCache(int id); 42 | 43 | /** 44 | * 根据库存 ID 查询数据库库存信息 45 | * @param id 46 | * @return 47 | */ 48 | Stock getStockById(int id); 49 | 50 | /** 51 | * 根据库存 ID 查询数据库库存信息(悲观锁) 52 | * @param id 53 | * @return 54 | */ 55 | Stock getStockByIdForUpdate(int id); 56 | 57 | /** 58 | * 更新数据库库存信息 59 | * @param stock 60 | * return 61 | */ 62 | int updateStockById(Stock stock); 63 | 64 | /** 65 | * 更新数据库库存信息(乐观锁) 66 | * @param stock 67 | * @return 68 | */ 69 | public int updateStockByOptimistic(Stock stock); 70 | 71 | } 72 | -------------------------------------------------------------------------------- /miaosha-service/src/main/java/cn/monitor4all/miaoshaservice/service/StockServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaservice.service; 2 | 3 | import cn.monitor4all.miaoshadao.dao.Stock; 4 | import cn.monitor4all.miaoshadao.mapper.StockMapper; 5 | import cn.monitor4all.miaoshadao.utils.CacheKey; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.data.redis.core.StringRedisTemplate; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.concurrent.TimeUnit; 13 | 14 | @Service 15 | public class StockServiceImpl implements StockService { 16 | 17 | private static final Logger LOGGER = LoggerFactory.getLogger(StockServiceImpl.class); 18 | 19 | @Autowired 20 | private StockMapper stockMapper; 21 | 22 | @Autowired 23 | private StringRedisTemplate stringRedisTemplate; 24 | 25 | @Override 26 | public Integer getStockCount(int sid) { 27 | Integer stockLeft; 28 | stockLeft = getStockCountByCache(sid); 29 | LOGGER.info("缓存中取得库存数:[{}]", stockLeft); 30 | if (stockLeft == null) { 31 | stockLeft = getStockCountByDB(sid); 32 | LOGGER.info("缓存未命中,查询数据库,并写入缓存"); 33 | setStockCountCache(sid, stockLeft); 34 | } 35 | return stockLeft; 36 | } 37 | 38 | @Override 39 | public int getStockCountByDB(int id) { 40 | Stock stock = stockMapper.selectByPrimaryKey(id); 41 | return stock.getCount() - stock.getSale(); 42 | } 43 | 44 | @Override 45 | public Integer getStockCountByCache(int id) { 46 | String hashKey = CacheKey.STOCK_COUNT.getKey() + "_" + id; 47 | String countStr = stringRedisTemplate.opsForValue().get(hashKey); 48 | if (countStr != null) { 49 | return Integer.parseInt(countStr); 50 | } else { 51 | return null; 52 | } 53 | } 54 | 55 | @Override 56 | public void setStockCountCache(int id, int count) { 57 | String hashKey = CacheKey.STOCK_COUNT.getKey() + "_" + id; 58 | String countStr = String.valueOf(count); 59 | LOGGER.info("写入商品库存缓存: [{}] [{}]", hashKey, countStr); 60 | stringRedisTemplate.opsForValue().set(hashKey, countStr, 3600, TimeUnit.SECONDS); 61 | } 62 | 63 | @Override 64 | public void delStockCountCache(int id) { 65 | String hashKey = CacheKey.STOCK_COUNT.getKey() + "_" + id; 66 | stringRedisTemplate.delete(hashKey); 67 | LOGGER.info("删除商品id:[{}] 缓存", id); 68 | } 69 | 70 | @Override 71 | public Stock getStockById(int id) { 72 | return stockMapper.selectByPrimaryKey(id); 73 | } 74 | 75 | @Override 76 | public Stock getStockByIdForUpdate(int id) { 77 | return stockMapper.selectByPrimaryKeyForUpdate(id); 78 | } 79 | 80 | @Override 81 | public int updateStockById(Stock stock) { 82 | return stockMapper.updateByPrimaryKeySelective(stock); 83 | } 84 | 85 | @Override 86 | public int updateStockByOptimistic(Stock stock) { 87 | return stockMapper.updateByOptimistic(stock); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /miaosha-service/src/main/java/cn/monitor4all/miaoshaservice/service/UserService.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaservice.service; 2 | 3 | public interface UserService { 4 | 5 | /** 6 | * 获取用户验证Hash 7 | * @param sid 8 | * @param userId 9 | * @return 10 | * @throws Exception 11 | */ 12 | public String getVerifyHash(Integer sid, Integer userId) throws Exception; 13 | 14 | /** 15 | * 添加用户访问次数 16 | * @param userId 17 | * @return 18 | * @throws Exception 19 | */ 20 | public int addUserCount(Integer userId) throws Exception; 21 | 22 | /** 23 | * 检查用户是否被禁 24 | * @param userId 25 | * @return 26 | */ 27 | public boolean getUserIsBanned(Integer userId); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /miaosha-service/src/main/java/cn/monitor4all/miaoshaservice/service/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaservice.service; 2 | 3 | import cn.monitor4all.miaoshadao.dao.Stock; 4 | import cn.monitor4all.miaoshadao.dao.User; 5 | import cn.monitor4all.miaoshadao.mapper.UserMapper; 6 | import cn.monitor4all.miaoshadao.utils.CacheKey; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.data.redis.core.StringRedisTemplate; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.util.DigestUtils; 13 | 14 | import java.util.concurrent.TimeUnit; 15 | 16 | @Service 17 | public class UserServiceImpl implements UserService { 18 | 19 | private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class); 20 | 21 | private static final String SALT = "randomString"; 22 | private static final int ALLOW_COUNT = 10; 23 | 24 | @Autowired 25 | private StringRedisTemplate stringRedisTemplate; 26 | 27 | @Autowired 28 | private UserMapper userMapper; 29 | 30 | @Autowired 31 | private StockService stockService; 32 | 33 | @Override 34 | public String getVerifyHash(Integer sid, Integer userId) throws Exception { 35 | 36 | // 验证是否在抢购时间内 37 | LOGGER.info("请自行验证是否在抢购时间内"); 38 | 39 | 40 | // 检查用户合法性 41 | User user = userMapper.selectByPrimaryKey(userId.longValue()); 42 | if (user == null) { 43 | throw new Exception("用户不存在"); 44 | } 45 | LOGGER.info("用户信息:[{}]", user.toString()); 46 | 47 | // 检查商品合法性 48 | Stock stock = stockService.getStockById(sid); 49 | if (stock == null) { 50 | throw new Exception("商品不存在"); 51 | } 52 | LOGGER.info("商品信息:[{}]", stock.toString()); 53 | 54 | // 生成hash 55 | String verify = SALT + sid + userId; 56 | String verifyHash = DigestUtils.md5DigestAsHex(verify.getBytes()); 57 | 58 | // 将hash和用户商品信息存入redis 59 | String hashKey = CacheKey.HASH_KEY.getKey() + "_" + sid + "_" + userId; 60 | stringRedisTemplate.opsForValue().set(hashKey, verifyHash, 3600, TimeUnit.SECONDS); 61 | LOGGER.info("Redis写入:[{}] [{}]", hashKey, verifyHash); 62 | return verifyHash; 63 | } 64 | 65 | @Override 66 | public int addUserCount(Integer userId) throws Exception { 67 | String limitKey = CacheKey.LIMIT_KEY.getKey() + "_" + userId; 68 | stringRedisTemplate.opsForValue().setIfAbsent(limitKey, "0", 3600, TimeUnit.SECONDS); 69 | Long limit = stringRedisTemplate.opsForValue().increment(limitKey); 70 | return Integer.parseInt(String.valueOf(limit)); 71 | } 72 | 73 | @Override 74 | public boolean getUserIsBanned(Integer userId) { 75 | String limitKey = CacheKey.LIMIT_KEY.getKey() + "_" + userId; 76 | String limitNum = stringRedisTemplate.opsForValue().get(limitKey); 77 | if (limitNum == null) { 78 | LOGGER.error("该用户没有访问申请验证值记录,疑似异常"); 79 | return true; 80 | } 81 | return Integer.parseInt(limitNum) > ALLOW_COUNT; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /miaosha-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.driver-class-name = com.mysql.jdbc.Driver 2 | spring.datasource.url = jdbc:mysql://localhost:3306/m4a_miaosha?characterEncoding=utf-8 3 | spring.datasource.username = root 4 | spring.datasource.password = root 5 | mybatis.mapper-locations=classpath:/mapper/*.xml 6 | 7 | spring.redis.host=localhost 8 | spring.redis.port=6379 9 | spring.redis.database=0 -------------------------------------------------------------------------------- /miaosha-service/src/main/resources/generatorConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 |
75 | 76 | 77 | -------------------------------------------------------------------------------- /miaosha-service/src/test/java/cn/monitor4all/miaoshaservice/MiaoshaServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaservice; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class MiaoshaServiceApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /miaosha-web/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /miaosha-web/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | cn.monitor4all 7 | miaosha 8 | 1.0.0-SNAPSHOT 9 | 10 | miaosha-web 11 | 12 | 13 | 1.8 14 | 15 | 16 | 17 | 18 | 19 | cn.monitor4all 20 | miaosha-service 21 | 1.0.0-SNAPSHOT 22 | compile 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-web 33 | 34 | 35 | 36 | com.google.guava 37 | guava 38 | 23.0 39 | 40 | 41 | 42 | com.alibaba 43 | fastjson 44 | 1.2.83 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-test 50 | test 51 | 52 | 53 | org.junit.vintage 54 | junit-vintage-engine 55 | 56 | 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-amqp 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-maven-plugin 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /miaosha-web/src/main/java/cn/monitor4all/miaoshaweb/MiaoshaWebApplication.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaweb; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication(scanBasePackages="cn.monitor4all") 7 | public class MiaoshaWebApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(MiaoshaWebApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /miaosha-web/src/main/java/cn/monitor4all/miaoshaweb/config/RabbitMqConfig.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaweb.config; 2 | 3 | import org.springframework.amqp.core.Queue; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * rabbitMq配置 9 | * 为方便初学者启动项目,暂时注释掉@Configuration,需要使用请去除@Configuration 10 | */ 11 | //@Configuration 12 | public class RabbitMqConfig { 13 | 14 | @Bean 15 | public Queue delCacheQueue() { 16 | return new Queue("delCache"); 17 | } 18 | 19 | @Bean 20 | public Queue orderQueue() { 21 | return new Queue("orderQueue"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /miaosha-web/src/main/java/cn/monitor4all/miaoshaweb/controller/OrderController.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaweb.controller; 2 | 3 | import cn.monitor4all.miaoshaservice.service.OrderService; 4 | import cn.monitor4all.miaoshaservice.service.StockService; 5 | import cn.monitor4all.miaoshaservice.service.UserService; 6 | import com.alibaba.fastjson.JSONObject; 7 | import com.google.common.util.concurrent.RateLimiter; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.amqp.core.AmqpTemplate; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Controller; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import java.util.concurrent.*; 16 | 17 | @Controller 18 | public class OrderController { 19 | 20 | private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class); 21 | 22 | @Autowired 23 | private OrderService orderService; 24 | 25 | @Autowired 26 | private UserService userService; 27 | 28 | @Autowired 29 | private StockService stockService; 30 | 31 | @Autowired 32 | private AmqpTemplate rabbitTemplate; 33 | 34 | // Guava令牌桶:每秒放行10个请求 35 | RateLimiter rateLimiter = RateLimiter.create(10); 36 | 37 | // 延时时间:预估读数据库数据业务逻辑的耗时,用来做缓存再删除 38 | private static final int DELAY_MILLSECONDS = 1000; 39 | 40 | // 延时双删线程池 41 | private static ExecutorService cachedThreadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue()); 42 | 43 | /** 44 | * 下单接口:导致超卖的错误示范 45 | * @param sid 46 | * @return 47 | */ 48 | @RequestMapping("/createWrongOrder/{sid}") 49 | @ResponseBody 50 | public String createWrongOrder(@PathVariable int sid) { 51 | int id = 0; 52 | try { 53 | id = orderService.createWrongOrder(sid); 54 | LOGGER.info("创建订单id: [{}]", id); 55 | } catch (Exception e) { 56 | LOGGER.error("Exception", e); 57 | } 58 | return String.valueOf(id); 59 | } 60 | 61 | /** 62 | * 下单接口:乐观锁更新库存 + 令牌桶限流 63 | * @param sid 64 | * @return 65 | */ 66 | @RequestMapping("/createOptimisticOrder/{sid}") 67 | @ResponseBody 68 | public String createOptimisticOrder(@PathVariable int sid) { 69 | // 1. 阻塞式获取令牌 70 | LOGGER.info("等待时间" + rateLimiter.acquire()); 71 | // 2. 非阻塞式获取令牌 72 | // if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) { 73 | // LOGGER.warn("你被限流了,真不幸,直接返回失败"); 74 | // return "你被限流了,真不幸,直接返回失败"; 75 | // } 76 | int id; 77 | try { 78 | id = orderService.createOptimisticOrder(sid); 79 | LOGGER.info("购买成功,剩余库存为: [{}]", id); 80 | } catch (Exception e) { 81 | LOGGER.error("购买失败:[{}]", e.getMessage()); 82 | return "购买失败,库存不足"; 83 | } 84 | return String.format("购买成功,剩余库存为:%d", id); 85 | } 86 | 87 | /** 88 | * 下单接口:悲观锁更新库存 事务for update更新库存 89 | * @param sid 90 | * @return 91 | */ 92 | @RequestMapping("/createPessimisticOrder/{sid}") 93 | @ResponseBody 94 | public String createPessimisticOrder(@PathVariable int sid) { 95 | int id; 96 | try { 97 | id = orderService.createPessimisticOrder(sid); 98 | LOGGER.info("购买成功,剩余库存为: [{}]", id); 99 | } catch (Exception e) { 100 | LOGGER.error("购买失败:[{}]", e.getMessage()); 101 | return "购买失败,库存不足"; 102 | } 103 | return String.format("购买成功,剩余库存为:%d", id); 104 | } 105 | 106 | /** 107 | * 验证接口:下单前用户获取验证值 108 | * @return 109 | */ 110 | @RequestMapping(value = "/getVerifyHash", method = {RequestMethod.GET}) 111 | @ResponseBody 112 | public String getVerifyHash(@RequestParam(value = "sid") Integer sid, 113 | @RequestParam(value = "userId") Integer userId) { 114 | String hash; 115 | try { 116 | hash = userService.getVerifyHash(sid, userId); 117 | } catch (Exception e) { 118 | LOGGER.error("获取验证hash失败,原因:[{}]", e.getMessage()); 119 | return "获取验证hash失败"; 120 | } 121 | return String.format("请求抢购验证hash值为:%s", hash); 122 | } 123 | 124 | /** 125 | * 下单接口:要求用户验证的抢购接口 126 | * @param sid 127 | * @return 128 | */ 129 | @RequestMapping(value = "/createOrderWithVerifiedUrl", method = {RequestMethod.GET}) 130 | @ResponseBody 131 | public String createOrderWithVerifiedUrl(@RequestParam(value = "sid") Integer sid, 132 | @RequestParam(value = "userId") Integer userId, 133 | @RequestParam(value = "verifyHash") String verifyHash) { 134 | int stockLeft; 135 | try { 136 | stockLeft = orderService.createVerifiedOrder(sid, userId, verifyHash); 137 | LOGGER.info("购买成功,剩余库存为: [{}]", stockLeft); 138 | } catch (Exception e) { 139 | LOGGER.error("购买失败:[{}]", e.getMessage()); 140 | return e.getMessage(); 141 | } 142 | return String.format("购买成功,剩余库存为:%d", stockLeft); 143 | } 144 | 145 | /** 146 | * 下单接口:要求验证的抢购接口 + 单用户限制访问频率 147 | * @param sid 148 | * @return 149 | */ 150 | @RequestMapping(value = "/createOrderWithVerifiedUrlAndLimit", method = {RequestMethod.GET}) 151 | @ResponseBody 152 | public String createOrderWithVerifiedUrlAndLimit(@RequestParam(value = "sid") Integer sid, 153 | @RequestParam(value = "userId") Integer userId, 154 | @RequestParam(value = "verifyHash") String verifyHash) { 155 | int stockLeft; 156 | try { 157 | int count = userService.addUserCount(userId); 158 | LOGGER.info("用户截至该次的访问次数为: [{}]", count); 159 | boolean isBanned = userService.getUserIsBanned(userId); 160 | if (isBanned) { 161 | return "购买失败,超过频率限制"; 162 | } 163 | stockLeft = orderService.createVerifiedOrder(sid, userId, verifyHash); 164 | LOGGER.info("购买成功,剩余库存为: [{}]", stockLeft); 165 | } catch (Exception e) { 166 | LOGGER.error("购买失败:[{}]", e.getMessage()); 167 | return e.getMessage(); 168 | } 169 | return String.format("购买成功,剩余库存为:%d", stockLeft); 170 | } 171 | 172 | /** 173 | * 下单接口:先删除缓存,再更新数据库 174 | * @param sid 175 | * @return 176 | */ 177 | @RequestMapping("/createOrderWithCacheV1/{sid}") 178 | @ResponseBody 179 | public String createOrderWithCacheV1(@PathVariable int sid) { 180 | int count = 0; 181 | try { 182 | // 删除库存缓存 183 | stockService.delStockCountCache(sid); 184 | // 完成扣库存下单事务 185 | count = orderService.createPessimisticOrder(sid); 186 | } catch (Exception e) { 187 | LOGGER.error("购买失败:[{}]", e.getMessage()); 188 | return "购买失败,库存不足"; 189 | } 190 | LOGGER.info("购买成功,剩余库存为: [{}]", count); 191 | return String.format("购买成功,剩余库存为:%d", count); 192 | } 193 | 194 | /** 195 | * 下单接口:先更新数据库,再删缓存 196 | * @param sid 197 | * @return 198 | */ 199 | @RequestMapping("/createOrderWithCacheV2/{sid}") 200 | @ResponseBody 201 | public String createOrderWithCacheV2(@PathVariable int sid) { 202 | int count = 0; 203 | try { 204 | // 完成扣库存下单事务 205 | count = orderService.createPessimisticOrder(sid); 206 | // 删除库存缓存 207 | stockService.delStockCountCache(sid); 208 | } catch (Exception e) { 209 | LOGGER.error("购买失败:[{}]", e.getMessage()); 210 | return "购买失败,库存不足"; 211 | } 212 | LOGGER.info("购买成功,剩余库存为: [{}]", count); 213 | return String.format("购买成功,剩余库存为:%d", count); 214 | } 215 | 216 | /** 217 | * 下单接口:先删除缓存,再更新数据库,缓存延时双删 218 | * @param sid 219 | * @return 220 | */ 221 | @RequestMapping("/createOrderWithCacheV3/{sid}") 222 | @ResponseBody 223 | public String createOrderWithCacheV3(@PathVariable int sid) { 224 | int count; 225 | try { 226 | // 删除库存缓存 227 | stockService.delStockCountCache(sid); 228 | // 完成扣库存下单事务 229 | count = orderService.createPessimisticOrder(sid); 230 | LOGGER.info("完成下单事务"); 231 | // 延时指定时间后再次删除缓存 232 | cachedThreadPool.execute(new delCacheByThread(sid)); 233 | } catch (Exception e) { 234 | LOGGER.error("购买失败:[{}]", e.getMessage()); 235 | return "购买失败,库存不足"; 236 | } 237 | LOGGER.info("购买成功,剩余库存为: [{}]", count); 238 | return String.format("购买成功,剩余库存为:%d", count); 239 | } 240 | 241 | /** 242 | * 下单接口:先更新数据库,再删缓存,删除缓存失败重试,通知消息队列 243 | * @param sid 244 | * @return 245 | */ 246 | @RequestMapping("/createOrderWithCacheV4/{sid}") 247 | @ResponseBody 248 | public String createOrderWithCacheV4(@PathVariable int sid) { 249 | int count; 250 | try { 251 | // 完成扣库存下单事务 252 | count = orderService.createPessimisticOrder(sid); 253 | LOGGER.info("完成下单事务"); 254 | // 删除库存缓存 255 | stockService.delStockCountCache(sid); 256 | // 延时指定时间后再次删除缓存 257 | // cachedThreadPool.execute(new delCacheByThread(sid)); 258 | // 假设上述再次删除缓存没成功,通知消息队列进行删除缓存 259 | sendToDelCache(String.valueOf(sid)); 260 | 261 | } catch (Exception e) { 262 | LOGGER.error("购买失败:[{}]", e.getMessage()); 263 | return "购买失败,库存不足"; 264 | } 265 | LOGGER.info("购买成功,剩余库存为: [{}]", count); 266 | return "购买成功"; 267 | } 268 | 269 | /** 270 | * 下单接口:异步处理订单 271 | * @param sid 272 | * @return 273 | */ 274 | @RequestMapping(value = "/createOrderWithMq", method = {RequestMethod.GET}) 275 | @ResponseBody 276 | public String createOrderWithMq(@RequestParam(value = "sid") Integer sid, 277 | @RequestParam(value = "userId") Integer userId) { 278 | try { 279 | // 检查缓存中商品是否还有库存 280 | Integer count = stockService.getStockCount(sid); 281 | if (count == 0) { 282 | return "秒杀请求失败,库存不足....."; 283 | } 284 | 285 | // 有库存,则将用户id和商品id封装为消息体传给消息队列处理 286 | // 注意这里的有库存和已经下单都是缓存中的结论,存在不可靠性,在消息队列中会查表再次验证 287 | LOGGER.info("有库存:[{}]", count); 288 | JSONObject jsonObject = new JSONObject(); 289 | jsonObject.put("sid", sid); 290 | jsonObject.put("userId", userId); 291 | sendToOrderQueue(jsonObject.toJSONString()); 292 | return "秒杀请求提交成功"; 293 | } catch (Exception e) { 294 | LOGGER.error("下单接口:异步处理订单异常:", e); 295 | return "秒杀请求失败,服务器正忙....."; 296 | } 297 | } 298 | 299 | /** 300 | * 下单接口:异步处理订单 301 | * @param sid 302 | * @return 303 | */ 304 | @RequestMapping(value = "/createUserOrderWithMq", method = {RequestMethod.GET}) 305 | @ResponseBody 306 | public String createUserOrderWithMq(@RequestParam(value = "sid") Integer sid, 307 | @RequestParam(value = "userId") Integer userId) { 308 | try { 309 | // 检查缓存中该用户是否已经下单过 310 | Boolean hasOrder = orderService.checkUserOrderInfoInCache(sid, userId); 311 | if (hasOrder != null && hasOrder) { 312 | LOGGER.info("该用户已经抢购过"); 313 | return "你已经抢购过了,不要太贪心....."; 314 | } 315 | // 没有下单过,检查缓存中商品是否还有库存 316 | LOGGER.info("没有抢购过,检查缓存中商品是否还有库存"); 317 | Integer count = stockService.getStockCount(sid); 318 | if (count == 0) { 319 | return "秒杀请求失败,库存不足....."; 320 | } 321 | 322 | // 有库存,则将用户id和商品id封装为消息体传给消息队列处理 323 | // 注意这里的有库存和已经下单都是缓存中的结论,存在不可靠性,在消息队列中会查表再次验证 324 | LOGGER.info("有库存:[{}]", count); 325 | JSONObject jsonObject = new JSONObject(); 326 | jsonObject.put("sid", sid); 327 | jsonObject.put("userId", userId); 328 | sendToOrderQueue(jsonObject.toJSONString()); 329 | return "秒杀请求提交成功"; 330 | } catch (Exception e) { 331 | LOGGER.error("下单接口:异步处理订单异常:", e); 332 | return "秒杀请求失败,服务器正忙....."; 333 | } 334 | } 335 | 336 | /** 337 | * 检查缓存中用户是否已经生成订单 338 | * @param sid 339 | * @return 340 | */ 341 | @RequestMapping(value = "/checkOrderByUserIdInCache", method = {RequestMethod.GET}) 342 | @ResponseBody 343 | public String checkOrderByUserIdInCache(@RequestParam(value = "sid") Integer sid, 344 | @RequestParam(value = "userId") Integer userId) { 345 | // 检查缓存中该用户是否已经下单过 346 | try { 347 | Boolean hasOrder = orderService.checkUserOrderInfoInCache(sid, userId); 348 | if (hasOrder != null && hasOrder) { 349 | return "恭喜您,已经抢购成功!"; 350 | } 351 | } catch (Exception e) { 352 | LOGGER.error("检查订单异常:", e); 353 | } 354 | return "很抱歉,你的订单尚未生成,继续排队。"; 355 | } 356 | 357 | 358 | /** 359 | * 缓存再删除线程 360 | */ 361 | private class delCacheByThread implements Runnable { 362 | private int sid; 363 | public delCacheByThread(int sid) { 364 | this.sid = sid; 365 | } 366 | public void run() { 367 | try { 368 | LOGGER.info("异步执行缓存再删除,商品id:[{}], 首先休眠:[{}] 毫秒", sid, DELAY_MILLSECONDS); 369 | Thread.sleep(DELAY_MILLSECONDS); 370 | stockService.delStockCountCache(sid); 371 | LOGGER.info("再次删除商品id:[{}] 缓存", sid); 372 | } catch (Exception e) { 373 | LOGGER.error("delCacheByThread执行出错", e); 374 | } 375 | } 376 | } 377 | 378 | 379 | /** 380 | * 向消息队列delCache发送消息 381 | * @param message 382 | */ 383 | private void sendToDelCache(String message) { 384 | LOGGER.info("这就去通知消息队列开始重试删除缓存:[{}]", message); 385 | this.rabbitTemplate.convertAndSend("delCache", message); 386 | } 387 | 388 | /** 389 | * 向消息队列orderQueue发送消息 390 | * @param message 391 | */ 392 | private void sendToOrderQueue(String message) { 393 | LOGGER.info("这就去通知消息队列开始下单:[{}]", message); 394 | this.rabbitTemplate.convertAndSend("orderQueue", message); 395 | } 396 | 397 | } 398 | -------------------------------------------------------------------------------- /miaosha-web/src/main/java/cn/monitor4all/miaoshaweb/controller/StockController.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaweb.controller; 2 | 3 | import cn.monitor4all.miaoshaservice.service.StockService; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.ResponseBody; 11 | 12 | @Controller 13 | public class StockController { 14 | 15 | private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class); 16 | 17 | @Autowired 18 | private StockService stockService; 19 | 20 | /** 21 | * 查询库存:通过数据库查询库存 22 | * @param sid 23 | * @return 24 | */ 25 | @RequestMapping("/getStockByDB/{sid}") 26 | @ResponseBody 27 | public String getStockByDB(@PathVariable int sid) { 28 | int count; 29 | try { 30 | count = stockService.getStockCountByDB(sid); 31 | } catch (Exception e) { 32 | LOGGER.error("查询库存失败:[{}]", e.getMessage()); 33 | return "查询库存失败"; 34 | } 35 | LOGGER.info("商品Id: [{}] 剩余库存为: [{}]", sid, count); 36 | return String.format("商品Id: %d 剩余库存为:%d", sid, count); 37 | } 38 | 39 | /** 40 | * 查询库存:通过缓存查询库存 41 | * 缓存命中:返回库存 42 | * 缓存未命中:查询数据库写入缓存并返回 43 | * @param sid 44 | * @return 45 | */ 46 | @RequestMapping("/getStockByCache/{sid}") 47 | @ResponseBody 48 | public String getStockByCache(@PathVariable int sid) { 49 | Integer count; 50 | try { 51 | count = stockService.getStockCount(sid); 52 | } catch (Exception e) { 53 | LOGGER.error("查询库存失败:[{}]", e.getMessage()); 54 | return "查询库存失败"; 55 | } 56 | LOGGER.info("商品Id: [{}] 剩余库存为: [{}]", sid, count); 57 | return String.format("商品Id: %d 剩余库存为:%d", sid, count); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /miaosha-web/src/main/java/cn/monitor4all/miaoshaweb/receiver/DelCacheReceiver.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaweb.receiver; 2 | 3 | import cn.monitor4all.miaoshaservice.service.StockService; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.amqp.rabbit.annotation.RabbitHandler; 7 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * rabbitMq接收者 13 | * 为方便初学者启动项目,暂时注释掉@Component,需要使用请去除@Component的注释 14 | */ 15 | //@Component 16 | @RabbitListener(queues = "delCache") 17 | public class DelCacheReceiver { 18 | 19 | private static final Logger LOGGER = LoggerFactory.getLogger(DelCacheReceiver.class); 20 | 21 | @Autowired 22 | private StockService stockService; 23 | 24 | @RabbitHandler 25 | public void process(String message) { 26 | LOGGER.info("DelCacheReceiver收到消息: " + message); 27 | LOGGER.info("DelCacheReceiver开始删除缓存: " + message); 28 | stockService.delStockCountCache(Integer.parseInt(message)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /miaosha-web/src/main/java/cn/monitor4all/miaoshaweb/receiver/OrderMqReceiver.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaweb.receiver; 2 | 3 | import cn.monitor4all.miaoshaservice.service.OrderService; 4 | import cn.monitor4all.miaoshaservice.service.StockService; 5 | import com.alibaba.fastjson.JSONObject; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.amqp.rabbit.annotation.RabbitHandler; 9 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | @Component 13 | @RabbitListener(queues = "orderQueue") 14 | public class OrderMqReceiver { 15 | 16 | private static final Logger LOGGER = LoggerFactory.getLogger(OrderMqReceiver.class); 17 | 18 | @Autowired 19 | private StockService stockService; 20 | 21 | @Autowired 22 | private OrderService orderService; 23 | 24 | @RabbitHandler 25 | public void process(String message) { 26 | LOGGER.info("OrderMqReceiver收到消息开始用户下单流程: " + message); 27 | JSONObject jsonObject = JSONObject.parseObject(message); 28 | try { 29 | orderService.createOrderByMq(jsonObject.getInteger("sid"),jsonObject.getInteger("userId")); 30 | } catch (Exception e) { 31 | LOGGER.error("消息处理异常:", e); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /miaosha-web/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.driver-class-name = com.mysql.jdbc.Driver 2 | spring.datasource.url = jdbc:mysql://localhost:3306/m4a_miaosha?characterEncoding=utf-8 3 | spring.datasource.username = root 4 | spring.datasource.password = root 5 | mybatis.mapper-locations=classpath:/mapper/*.xml 6 | 7 | server.tomcat.max-threads=10000 8 | server.tomcat.max-connections=10000 9 | 10 | spring.rabbitmq.host=localhost 11 | spring.rabbitmq.port=5672 12 | spring.rabbitmq.username=admin 13 | spring.rabbitmq.password=123456 -------------------------------------------------------------------------------- /miaosha-web/src/test/java/cn/monitor4all/miaoshaweb/MiaoshaWebApplicationTests.java: -------------------------------------------------------------------------------- 1 | package cn.monitor4all.miaoshaweb; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | import java.util.concurrent.ExecutorService; 8 | 9 | @SpringBootTest 10 | class MiaoshaWebApplicationTests { 11 | 12 | private static Logger logger = LoggerFactory.getLogger(MiaoshaWebApplicationTests.class); 13 | private static ExecutorService executorServicePool; 14 | private static String url = "http://127.0.0.1:8080/createOptimisticLimitOrder/1" ; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /miaosha.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | -- ---------------------------- 4 | -- Table structure for stock 5 | -- ---------------------------- 6 | DROP TABLE IF EXISTS `stock`; 7 | CREATE TABLE `stock` ( 8 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 9 | `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称', 10 | `count` int(11) NOT NULL COMMENT '库存', 11 | `sale` int(11) NOT NULL COMMENT '已售', 12 | `version` int(11) NOT NULL COMMENT '乐观锁,版本号', 13 | PRIMARY KEY (`id`) 14 | ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; 15 | 16 | -- ---------------------------- 17 | -- Records of stock 18 | -- ---------------------------- 19 | INSERT INTO `stock` VALUES ('1', 'iphone', '50', '0', '0'); 20 | INSERT INTO `stock` VALUES ('2', 'mac', '10', '0', '0'); 21 | 22 | -- ---------------------------- 23 | -- Table structure for stock_order 24 | -- ---------------------------- 25 | DROP TABLE IF EXISTS `stock_order`; 26 | CREATE TABLE `stock_order` ( 27 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 28 | `sid` int(11) NOT NULL COMMENT '库存ID', 29 | `name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称', 30 | `user_id` int(11) NOT NULL DEFAULT '0', 31 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', 32 | PRIMARY KEY (`id`) 33 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 34 | 35 | -- ---------------------------- 36 | -- Records of stock_order 37 | -- ---------------------------- 38 | 39 | -- ---------------------------- 40 | -- Table structure for user 41 | -- ---------------------------- 42 | DROP TABLE IF EXISTS `user`; 43 | CREATE TABLE `user` ( 44 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 45 | `user_name` varchar(255) NOT NULL DEFAULT '', 46 | PRIMARY KEY (`id`) 47 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; 48 | 49 | -- ---------------------------- 50 | -- Records of user 51 | -- ---------------------------- 52 | INSERT INTO `user` VALUES ('1', '张三'); 53 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.5.RELEASE 9 | 10 | 11 | 12 | 4.0.0 13 | cn.monitor4all 14 | miaosha 15 | 1.0.0-SNAPSHOT 16 | miaosha 17 | pom 18 | 19 | 20 | miaosha-dao 21 | miaosha-service 22 | miaosha-web 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------