├── .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 |
--------------------------------------------------------------------------------