├── .gitignore
├── LICENSE
├── README.md
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── example
│ │ ├── Application.java
│ │ ├── common
│ │ ├── IBaseService.java
│ │ ├── ResponseBean.java
│ │ └── impl
│ │ │ └── BaseServiceImpl.java
│ │ ├── config
│ │ ├── ExceptionAdvice.java
│ │ └── redis
│ │ │ └── JedisConfig.java
│ │ ├── constant
│ │ └── Constant.java
│ │ ├── controller
│ │ ├── LimitController.java
│ │ ├── RedisLockController.java
│ │ ├── SeckillEvolutionController.java
│ │ ├── StockController.java
│ │ └── StockOrderController.java
│ │ ├── dao
│ │ ├── StockDao.java
│ │ └── StockOrderDao.java
│ │ ├── dto
│ │ ├── custom
│ │ │ ├── StockDto.java
│ │ │ └── StockOrderDto.java
│ │ └── domain
│ │ │ ├── StockDtoBase.java
│ │ │ └── StockOrderDtoBase.java
│ │ ├── exception
│ │ ├── CustomException.java
│ │ └── SystemException.java
│ │ ├── limit
│ │ ├── Limit.java
│ │ └── LimitAspect.java
│ │ ├── seckill
│ │ ├── ISeckillService.java
│ │ └── impl
│ │ │ ├── SeckillOptimisticLockRedisSafeServiceImpl.java
│ │ │ ├── SeckillOptimisticLockRedisWrongServiceImpl.java
│ │ │ ├── SeckillOptimisticLockServiceImpl.java
│ │ │ └── SeckillTraditionServiceImpl.java
│ │ ├── service
│ │ ├── ISeckillEvolutionService.java
│ │ ├── IStockOrderService.java
│ │ ├── IStockService.java
│ │ └── impl
│ │ │ ├── SeckillEvolutionServiceImpl.java
│ │ │ ├── StockOrderServiceImpl.java
│ │ │ └── StockServiceImpl.java
│ │ └── util
│ │ ├── JedisUtil.java
│ │ └── RedisLimitUtil.java
└── resources
│ ├── application.yml
│ ├── jmx
│ ├── 乐观锁加缓存加限流秒杀测试.jmx
│ ├── 乐观锁加缓存秒杀测试.jmx
│ ├── 乐观锁秒杀测试.jmx
│ └── 传统方式秒杀测试.jmx
│ ├── mapper
│ ├── StockDao.xml
│ └── StockOrderDao.xml
│ ├── redis
│ ├── limit-custom.lua
│ └── limit-seckill.lua
│ └── sql
│ └── MySQL.sql
└── test
└── java
└── com
└── example
└── ApplicationTests.java
/.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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 随心
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SeckillEvolution
2 |
3 | [](LICENSE)
4 | [](https://github.com/dolyw/SeckillEvolution/pulls)
5 | [](https://github.com/dolyw/SeckillEvolution)
6 | [](https://github.com/dolyw/SeckillEvolution)
7 |
8 | > 一个简单的秒杀架构的演变
9 |
10 | #### 项目介绍
11 |
12 | 从零开始搭建一个简单的秒杀后台,以及持续优化性能
13 |
14 | * 文章: [https://note.dolyw.com/seckill-evolution/](https://note.dolyw.com/seckill-evolution/)
15 | * Github:[https://github.com/dolyw/SeckillEvolution](https://github.com/dolyw/SeckillEvolution)
16 | * Gitee(码云):[https://gitee.com/dolyw/SeckillEvolution](https://gitee.com/dolyw/SeckillEvolution)
17 |
18 | #### 项目目录
19 |
20 | * [0. 整体流程](https://note.dolyw.com/seckill-evolution/00-Preparation.html)
21 | * [1. 传统方式](https://note.dolyw.com/seckill-evolution/01-Tradition-Process.html)
22 | * [2. 使用乐观锁](https://note.dolyw.com/seckill-evolution/02-Optimistic-Lock.html)
23 | * [3. 使用缓存](https://note.dolyw.com/seckill-evolution/03-Optimistic-Lock-Redis.html)
24 | * [4. 使用分布式限流](https://note.dolyw.com/seckill-evolution/04-Distributed-Limit.html)
25 | * [5. 使用队列异步下单](https://note.dolyw.com/seckill-evolution/05-MQ-Async.html)
26 |
27 | **其他**
28 |
29 | * [JMeter的安装使用](https://note.dolyw.com/command/06-JMeter-Install.html)
30 | * [MySQL那些锁](http://note.dolyw.com/database/01-MySQL-Lock.html)
31 | * [Redis与数据库一致性](https://note.dolyw.com/cache/00-DataBaseConsistency.html)
32 | * [高并发下的限流分析](http://note.dolyw.com/seckill/02-Distributed-Limit.html)
33 |
34 | #### 软件架构
35 |
36 | 1. SpringBoot + Mybatis核心框架
37 | 2. PageHelper插件 + 通用Mapper插件
38 | 3. Redis(Jedis)缓存框架
39 | 4. 消息队列
40 |
41 | #### 安装教程
42 |
43 | 1. 数据库帐号密码默认为root,如有修改,请自行修改配置文件application.yml
44 | 2. 解压后执行src\main\resources\sql\MySQL.sql脚本创建数据库和表
45 | 3. Redis需要自行安装Redis服务,端口密码默认
46 | 4. SpringBoot直接启动即可,测试工具PostMan,JMeter
47 | 5. JMeter测试计划文件在src\main\resources\jmx下
48 |
49 | #### 搭建参考
50 |
51 | * 感谢杨冠标的流量削峰: [https://www.cnblogs.com/yanggb/p/11117400.html](https://www.cnblogs.com/yanggb/p/11117400.html)
52 | * 感谢mikechen优知的高并发架构系列:什么是流量削峰?如何解决秒杀业务的削峰场景: [https://www.jianshu.com/p/6746140bbb76](https://www.jianshu.com/p/6746140bbb76)
53 | * 感谢crossoverjie的SSM(十八) 秒杀架构实践: [https://crossoverjie.top/2018/05/07/ssm/SSM18-seconds-kill/](https://crossoverjie.top/2018/05/07/ssm/SSM18-seconds-kill/)
54 | * 感谢crossoverjie的设计一个秒杀系统思路以及限流: [https://github.com/crossoverJie/JCSprout/blob/master/MD/Spike.md](https://github.com/crossoverJie/JCSprout/blob/master/MD/Spike.md)
55 | * 感谢qiurunze123的秒杀系统设计与实现: [https://github.com/qiurunze123/miaosha](https://github.com/qiurunze123/miaosha)
56 |
57 | #### 参与贡献
58 |
59 | 1. Fork 本项目
60 | 2. 新建 Feat_xxx 分支
61 | 3. 提交代码
62 | 4. 新建 Pull Request
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.1.3.RELEASE
9 |
10 |
11 | com.example
12 | SeckillEvolution
13 | 0.0.1-SNAPSHOT
14 | SeckillEvolution
15 | Demo project for Spring Boot
16 |
17 |
18 | 1.8
19 | UTF-8
20 | 1.3.1
21 | 1.1.9
22 | 1.2.3
23 | 1.2.3
24 | 1.2.47
25 | 3.7
26 | 2.9.0
27 |
28 |
29 |
30 |
31 |
32 | org.springframework.boot
33 | spring-boot-starter
34 |
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-starter-test
39 | test
40 |
41 |
42 |
43 |
44 | org.springframework.boot
45 | spring-boot-devtools
46 | true
47 |
48 |
49 |
50 |
51 | org.springframework.boot
52 | spring-boot-starter-web
53 |
54 |
55 |
56 |
57 | mysql
58 | mysql-connector-java
59 |
60 |
61 |
62 |
63 | org.mybatis.spring.boot
64 | mybatis-spring-boot-starter
65 | ${mybatis.version}
66 |
67 |
68 |
69 |
70 | com.alibaba
71 | druid-spring-boot-starter
72 | ${druid.version}
73 |
74 |
75 |
76 |
77 | com.github.pagehelper
78 | pagehelper-spring-boot-starter
79 | ${pagehelper.version}
80 |
81 |
82 |
83 |
84 | tk.mybatis
85 | mapper-spring-boot-starter
86 | ${mapper.version}
87 |
88 |
89 |
90 |
91 | com.alibaba
92 | fastjson
93 | ${fastjson.version}
94 |
95 |
96 |
97 |
98 | org.apache.commons
99 | commons-lang3
100 | ${commons-lang3.version}
101 |
102 |
103 |
104 |
105 | redis.clients
106 | jedis
107 | ${jedis.version}
108 |
109 |
110 |
111 |
112 | org.springframework.boot
113 | spring-boot-starter-aop
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | org.springframework.boot
123 | spring-boot-maven-plugin
124 |
125 |
126 |
127 | org.apache.maven.plugins
128 | maven-compiler-plugin
129 |
130 | ${java.version}
131 | ${java.version}
132 | ${project.build.sourceEncoding}
133 |
134 |
135 |
136 |
137 | org.apache.maven.plugins
138 | maven-javadoc-plugin
139 | 3.0.0
140 |
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/src/main/java/com/example/Application.java:
--------------------------------------------------------------------------------
1 | package com.example;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | /**
7 | * Application
8 | *
9 | * @author wliduo[i@dolyw.com]
10 | * @date 2019/7/31 18:00
11 | */
12 | @SpringBootApplication
13 | @tk.mybatis.spring.annotation.MapperScan("com.example.dao")
14 | public class Application {
15 |
16 | public static void main(String[] args) {
17 | SpringApplication.run(Application.class, args);
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/com/example/common/IBaseService.java:
--------------------------------------------------------------------------------
1 | package com.example.common;
2 |
3 | import org.apache.ibatis.annotations.Param;
4 | import org.apache.ibatis.session.RowBounds;
5 |
6 | import java.util.List;
7 |
8 | /**
9 | * IBaseService
10 | * @author dolyw.com
11 | * @date 2018/8/9 15:45
12 | */
13 | public interface IBaseService {
14 |
15 | // Select
16 | /**
17 | * 根据实体中的属性值进行查询,查询条件使用等号
18 | * @param record
19 | * @return java.util.List
20 | * @author dolyw.com
21 | * @date 2018/8/9 15:43
22 | */
23 | List select(T record);
24 |
25 | /**
26 | * 根据主键字段进行查询,方法参数必须包含完整的主键属性,查询条件使用等号
27 | * @param key
28 | * @return T
29 | * @author dolyw.com
30 | * @date 2018/8/9 15:43
31 | */
32 | T selectByPrimaryKey(Object key);
33 |
34 | /**
35 | * 查询全部结果,select(null)方法能达到同样的效果
36 | * @param
37 | * @return java.util.List
38 | * @author dolyw.com
39 | * @date 2018/8/9 15:43
40 | */
41 | List selectAll();
42 |
43 | /**
44 | * 根据实体中的属性进行查询,只能有一个返回值,有多个结果是抛出异常,查询条件使用等号
45 | * @param record
46 | * @return T
47 | * @author dolyw.com
48 | * @date 2018/8/9 15:43
49 | */
50 | T selectOne(T record);
51 |
52 | /**
53 | * 根据实体中的属性查询总数,查询条件使用等号
54 | * @param record
55 | * @return int
56 | * @author dolyw.com
57 | * @date 2018/8/9 15:43
58 | */
59 | int selectCount(T record);
60 |
61 | // Insert
62 | /**
63 | * 保存一个实体,null的属性也会保存,不会使用数据库默认值
64 | * @param record
65 | * @return int
66 | * @author dolyw.com
67 | * @date 2018/8/9 15:43
68 | */
69 | int insert(T record);
70 |
71 | /**
72 | * 保存一个实体,null的属性不会保存,会使用数据库默认值
73 | * @param record
74 | * @return int
75 | * @author dolyw.com
76 | * @date 2018/8/9 15:43
77 | */
78 | int insertSelective(T record);
79 |
80 | // Update
81 | /**
82 | * 根据主键更新实体全部字段,null值会被更新
83 | * @param record
84 | * @return int
85 | * @author dolyw.com
86 | * @date 2018/8/9 15:43
87 | */
88 | int updateByPrimaryKey(T record);
89 |
90 | /**
91 | * 根据主键更新属性不为null的值
92 | * @param record
93 | * @return int
94 | * @author dolyw.com
95 | * @date 2018/8/9 15:43
96 | */
97 | int updateByPrimaryKeySelective(T record);
98 |
99 | // Delete
100 | /**
101 | * 根据实体属性作为条件进行删除,查询条件使用等号
102 | * @param record
103 | * @return int
104 | * @author dolyw.com
105 | * @date 2018/8/9 15:43
106 | */
107 | int delete(T record);
108 |
109 | /**
110 | * 根据主键字段进行删除,方法参数必须包含完整的主键属性
111 | * @param key
112 | * @return int
113 | * @author dolyw.com
114 | * @date 2018/8/9 15:44
115 | */
116 | int deleteByPrimaryKey(Object key);
117 |
118 | // Example
119 | /**
120 | * 根据Example条件进行查询,这个查询支持通过Example类指定查询列,通过selectProperties方法指定查询列
121 | * @param example
122 | * @return java.util.List
123 | * @author dolyw.com
124 | * @date 2018/8/9 15:44
125 | */
126 | List selectByExample(Object example);
127 |
128 | /**
129 | * 根据Example条件进行查询总数
130 | * @param example
131 | * @return int
132 | * @author dolyw.com
133 | * @date 2018/8/9 15:44
134 | */
135 | int selectCountByExample(Object example);
136 |
137 | /**
138 | * 根据Example条件更新实体record包含的全部属性,null值会被更新
139 | * @param record
140 | * @param example
141 | * @return int
142 | * @author dolyw.com
143 | * @date 2018/8/9 15:44
144 | */
145 | int updateByExample(@Param("record") T record, @Param("example") Object example);
146 |
147 | /**
148 | * 根据Example条件更新实体record包含的不是null的属性值
149 | * @param record
150 | * @param example
151 | * @return int
152 | * @author dolyw.com
153 | * @date 2018/8/9 15:44
154 | */
155 | int updateByExampleSelective(@Param("record") T record, @Param("example") Object example);
156 |
157 | /**
158 | * 根据Example条件删除数据
159 | * @param example
160 | * @return int
161 | * @author dolyw.com
162 | * @date 2018/8/9 15:44
163 | */
164 | int deleteByExample(Object example);
165 |
166 | // RowBounds
167 | /**
168 | * 根据实体属性和RowBounds进行分页查询
169 | * @param record
170 | * @param rowBounds
171 | * @return java.util.List
172 | * @author dolyw.com
173 | * @date 2018/8/9 15:44
174 | */
175 | List selectByRowBounds(T record, RowBounds rowBounds);
176 |
177 | /**
178 | * 根据example条件和RowBounds进行分页查询
179 | * @param example
180 | * @param rowBounds
181 | * @return java.util.List
182 | * @author dolyw.com
183 | * @date 2018/8/9 15:44
184 | */
185 | List selectByExampleAndRowBounds(Object example, RowBounds rowBounds);
186 | }
187 |
--------------------------------------------------------------------------------
/src/main/java/com/example/common/ResponseBean.java:
--------------------------------------------------------------------------------
1 | package com.example.common;
2 |
3 | /**
4 | * ResponseBean
5 | *
6 | * @author wliduo[i@dolyw.com]
7 | * @date 2018/8/30 11:39
8 | */
9 | public class ResponseBean {
10 | /**
11 | * HTTP状态码
12 | */
13 | private Integer code;
14 |
15 | /**
16 | * 返回信息
17 | */
18 | private String msg;
19 |
20 | /**
21 | * 返回的数据
22 | */
23 | private Object data;
24 |
25 | public ResponseBean(int code, String msg, Object data) {
26 | this.code = code;
27 | this.msg = msg;
28 | this.data = data;
29 | }
30 |
31 | public Integer getCode() {
32 | return code;
33 | }
34 |
35 | public void setCode(Integer code) {
36 | this.code = code;
37 | }
38 |
39 | public String getMsg() {
40 | return msg;
41 | }
42 |
43 | public void setMsg(String msg) {
44 | this.msg = msg;
45 | }
46 |
47 | public Object getData() {
48 | return data;
49 | }
50 |
51 | public void setData(Object data) {
52 | this.data = data;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/com/example/common/impl/BaseServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.example.common.impl;
2 |
3 | import com.example.common.IBaseService;
4 | import org.apache.ibatis.session.RowBounds;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import tk.mybatis.mapper.common.Mapper;
7 |
8 | import java.util.List;
9 |
10 | /**
11 | * BaseServiceImpl
12 | * @author dolyw.com
13 | * @date 2018/8/9 15:45
14 | */
15 | public abstract class BaseServiceImpl implements IBaseService {
16 |
17 | @Autowired
18 | protected Mapper mapper;
19 |
20 | public Mapper getMapper() {
21 | return mapper;
22 | }
23 |
24 | @Override
25 | public List select(T record) {
26 | return mapper.select(record);
27 | }
28 |
29 | @Override
30 | public T selectByPrimaryKey(Object key) {
31 | return mapper.selectByPrimaryKey(key);
32 | }
33 |
34 | @Override
35 | public List selectAll() {
36 | return mapper.selectAll();
37 | }
38 |
39 | @Override
40 | public T selectOne(T record) {
41 | return mapper.selectOne(record);
42 | }
43 |
44 | @Override
45 | public int selectCount(T record) {
46 | return mapper.selectCount(record);
47 | }
48 |
49 | @Override
50 | public int insert(T record) {
51 | return mapper.insert(record);
52 | }
53 |
54 | @Override
55 | public int insertSelective(T record) {
56 | return mapper.insertSelective(record);
57 | }
58 |
59 | @Override
60 | public int updateByPrimaryKey(T record) {
61 | return mapper.updateByPrimaryKey(record);
62 | }
63 |
64 | @Override
65 | public int updateByPrimaryKeySelective(T record) {
66 | return mapper.updateByPrimaryKeySelective(record);
67 | }
68 |
69 | @Override
70 | public int delete(T record) {
71 | return mapper.delete(record);
72 | }
73 |
74 | @Override
75 | public int deleteByPrimaryKey(Object key) {
76 | return mapper.deleteByPrimaryKey(key);
77 | }
78 |
79 | @Override
80 | public List selectByExample(Object example) {
81 | return mapper.selectByExample(example);
82 | }
83 |
84 | @Override
85 | public int selectCountByExample(Object example) {
86 | return mapper.selectCountByExample(example);
87 | }
88 |
89 | @Override
90 | public int updateByExample(T record, Object example) {
91 | return mapper.updateByExample(record, example);
92 | }
93 |
94 | @Override
95 | public int updateByExampleSelective(T record, Object example) {
96 | return mapper.updateByExampleSelective(record, example);
97 | }
98 |
99 | @Override
100 | public int deleteByExample(Object example) {
101 | return mapper.deleteByExample(example);
102 | }
103 |
104 | @Override
105 | public List selectByRowBounds(T record, RowBounds rowBounds) {
106 | return mapper.selectByRowBounds(record, rowBounds);
107 | }
108 |
109 | @Override
110 | public List selectByExampleAndRowBounds(Object example, RowBounds rowBounds) {
111 | return mapper.selectByExampleAndRowBounds(example, rowBounds);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/java/com/example/config/ExceptionAdvice.java:
--------------------------------------------------------------------------------
1 | package com.example.config;
2 |
3 | import com.example.common.ResponseBean;
4 | import com.example.exception.CustomException;
5 | import com.example.exception.SystemException;
6 | import org.springframework.http.HttpStatus;
7 | import org.springframework.web.bind.annotation.ExceptionHandler;
8 | import org.springframework.web.bind.annotation.ResponseStatus;
9 | import org.springframework.web.bind.annotation.RestControllerAdvice;
10 | import org.springframework.web.servlet.NoHandlerFoundException;
11 |
12 | import javax.servlet.http.HttpServletRequest;
13 |
14 | /**
15 | * 异常控制处理器
16 | *
17 | * @author wliduo[i@dolyw.com]
18 | * @date 2018/8/30 14:02
19 | */
20 | @RestControllerAdvice
21 | public class ExceptionAdvice {
22 |
23 | /**
24 | * 捕捉自定义异常
25 | * @return
26 | */
27 | @ResponseStatus(HttpStatus.BAD_REQUEST)
28 | @ExceptionHandler(CustomException.class)
29 | public ResponseBean handle(CustomException e) {
30 | return new ResponseBean(HttpStatus.BAD_REQUEST.value(), e.getMessage(), null);
31 | }
32 |
33 | /**
34 | * 捕捉系统异常
35 | * @return
36 | */
37 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
38 | @ExceptionHandler(SystemException.class)
39 | public ResponseBean handle(SystemException e) {
40 | return new ResponseBean(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage(), null);
41 | }
42 |
43 | /**
44 | * 捕捉404异常
45 | * @return
46 | */
47 | @ResponseStatus(HttpStatus.NOT_FOUND)
48 | @ExceptionHandler(NoHandlerFoundException.class)
49 | public ResponseBean handle(NoHandlerFoundException e) {
50 | return new ResponseBean(HttpStatus.NOT_FOUND.value(), e.getMessage(), null);
51 | }
52 |
53 | /**
54 | * 捕捉其他所有异常
55 | * @param request
56 | * @param ex
57 | * @return
58 | */
59 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
60 | @ExceptionHandler(Exception.class)
61 | public ResponseBean globalException(HttpServletRequest request, Throwable ex) {
62 | return new ResponseBean(this.getStatus(request).value(), ex.toString() + ": " + ex.getMessage(), null);
63 | }
64 |
65 | /**
66 | * 获取状态码
67 | * @param request
68 | * @return
69 | */
70 | private HttpStatus getStatus(HttpServletRequest request) {
71 | Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
72 | if (statusCode == null) {
73 | return HttpStatus.INTERNAL_SERVER_ERROR;
74 | }
75 | return HttpStatus.valueOf(statusCode);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/com/example/config/redis/JedisConfig.java:
--------------------------------------------------------------------------------
1 | package com.example.config.redis;
2 |
3 | import org.apache.commons.lang3.StringUtils;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.beans.factory.annotation.Value;
7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
8 | import org.springframework.boot.context.properties.ConfigurationProperties;
9 | import org.springframework.context.annotation.Bean;
10 | import org.springframework.context.annotation.Configuration;
11 | import redis.clients.jedis.JedisPool;
12 | import redis.clients.jedis.JedisPoolConfig;
13 |
14 | /**
15 | * Jedis配置,项目启动注入JedisPool
16 | *
17 | * @author dolyw.com
18 | * @date 2018/9/5 10:35
19 | */
20 | @Configuration
21 | @EnableAutoConfiguration
22 | @ConfigurationProperties(prefix = "redis")
23 | public class JedisConfig {
24 |
25 | /**
26 | * logger
27 | */
28 | private static final Logger logger = LoggerFactory.getLogger(JedisConfig.class);
29 |
30 | private String host;
31 |
32 | private int port;
33 |
34 | private String password;
35 |
36 | private int timeout;
37 |
38 | @Value("${redis.pool.max-active}")
39 | private int maxActive;
40 |
41 | @Value("${redis.pool.max-wait}")
42 | private int maxWait;
43 |
44 | @Value("${redis.pool.max-idle}")
45 | private int maxIdle;
46 |
47 | @Value("${redis.pool.min-idle}")
48 | private int minIdle;
49 |
50 | @Bean
51 | public JedisPool redisPoolFactory() {
52 | try {
53 | JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
54 | jedisPoolConfig.setMaxIdle(maxIdle);
55 | jedisPoolConfig.setMaxWaitMillis(maxWait);
56 | jedisPoolConfig.setMaxTotal(maxActive);
57 | jedisPoolConfig.setMinIdle(minIdle);
58 | // 密码为空设置为null
59 | if (StringUtils.isBlank(password)) {
60 | password = null;
61 | }
62 | JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
63 | logger.info("初始化Redis连接池JedisPool成功!地址: " + host + ":" + port);
64 | return jedisPool;
65 | } catch (Exception e) {
66 | logger.error("初始化Redis连接池JedisPool异常:" + e.getMessage());
67 | }
68 | return null;
69 | }
70 |
71 | public String getHost() {
72 | return host;
73 | }
74 |
75 | public void setHost(String host) {
76 | this.host = host;
77 | }
78 |
79 | public int getPort() {
80 | return port;
81 | }
82 |
83 | public void setPort(int port) {
84 | this.port = port;
85 | }
86 |
87 | public String getPassword() {
88 | return password;
89 | }
90 |
91 | public void setPassword(String password) {
92 | this.password = password;
93 | }
94 |
95 | public int getTimeout() {
96 | return timeout;
97 | }
98 |
99 | public void setTimeout(int timeout) {
100 | this.timeout = timeout;
101 | }
102 |
103 | public int getMaxActive() {
104 | return maxActive;
105 | }
106 |
107 | public void setMaxActive(int maxActive) {
108 | this.maxActive = maxActive;
109 | }
110 |
111 | public int getMaxWait() {
112 | return maxWait;
113 | }
114 |
115 | public void setMaxWait(int maxWait) {
116 | this.maxWait = maxWait;
117 | }
118 |
119 | public int getMaxIdle() {
120 | return maxIdle;
121 | }
122 |
123 | public void setMaxIdle(int maxIdle) {
124 | this.maxIdle = maxIdle;
125 | }
126 |
127 | public int getMinIdle() {
128 | return minIdle;
129 | }
130 |
131 | public void setMinIdle(int minIdle) {
132 | this.minIdle = minIdle;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/main/java/com/example/constant/Constant.java:
--------------------------------------------------------------------------------
1 | package com.example.constant;
2 |
3 | /**
4 | * 常量
5 | *
6 | * @author wliduo[i@dolyw.com]
7 | * @date 2018/9/3 16:03
8 | */
9 | public interface Constant {
10 |
11 | /**
12 | * redis-OK
13 | */
14 | String OK = "OK";
15 |
16 | /**
17 | * redis过期时间,以秒为单位,一分钟
18 | */
19 | int EXPIRE_MINUTE = 60;
20 |
21 | /**
22 | * redis过期时间,以秒为单位,一小时
23 | */
24 | int EXPIRE_HOUR = 60 * 60;
25 |
26 | /**
27 | * redis过期时间,以秒为单位,一天
28 | */
29 | int EXPIRE_DAY = 60 * 60 * 24;
30 |
31 | /**
32 | * redis-key-示例前缀-example
33 | */
34 | String PREFIX_EXAMPLE = "example:";
35 |
36 | /**
37 | * 商品名称
38 | */
39 | String ITEM_STOCK_NAME = "OnePlus 7 Pro";
40 |
41 | /**
42 | * redis-key-前缀-count-库存
43 | */
44 | String PREFIX_COUNT = "stock:count:";
45 |
46 | /**
47 | * redis-key-前缀-sale-已售
48 | */
49 | String PREFIX_SALE = "stock:sale:";
50 |
51 | /**
52 | * redis-key-前缀-version-乐观锁版本
53 | */
54 | String PREFIX_VERSION = "stock:version:";
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/com/example/controller/LimitController.java:
--------------------------------------------------------------------------------
1 | package com.example.controller;
2 |
3 | import com.example.limit.Limit;
4 | import com.example.util.RedisLimitUtil;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.web.bind.annotation.GetMapping;
9 | import org.springframework.web.bind.annotation.RequestMapping;
10 | import org.springframework.web.bind.annotation.RestController;
11 |
12 | import java.util.concurrent.atomic.AtomicInteger;
13 | import java.util.concurrent.atomic.AtomicLong;
14 |
15 | /**
16 | * 计数器(固定时间窗口)限流接口测试
17 | *
18 | * @author wliduo[i@dolyw.com]
19 | * @date 2019/11/24 19:27
20 | */
21 | @RestController
22 | @RequestMapping("/limit")
23 | public class LimitController {
24 |
25 | /**
26 | * logger
27 | */
28 | private static final Logger logger = LoggerFactory.getLogger(LimitController.class);
29 |
30 | /**
31 | * 一个时间窗口内最大请求数(限流最大请求数)
32 | */
33 | private static final Long MAX_NUM_REQUEST = 2L;
34 |
35 | /**
36 | * 一个时间窗口时间(毫秒)(限流时间)
37 | */
38 | private static final Long TIME_REQUEST = 5000L;
39 |
40 | /**
41 | * 一个时间窗口内请求的数量累计(限流请求数累计)
42 | */
43 | private AtomicInteger requestNum = new AtomicInteger(0);
44 |
45 | /**
46 | * 一个时间窗口开始时间(限流开始时间)
47 | */
48 | private AtomicLong requestTime = new AtomicLong(System.currentTimeMillis());
49 |
50 | /**
51 | * RedisLimitUtil
52 | */
53 | @Autowired
54 | private RedisLimitUtil redisLimitUtil;
55 |
56 | /**
57 | * 计数器(固定时间窗口)请求接口
58 | *
59 | * @param
60 | * @return java.lang.String
61 | * @throws
62 | * @author wliduo[i@dolyw.com]
63 | * @date 2019/11/25 16:19
64 | */
65 | @GetMapping
66 | public String index() {
67 | long nowTime = System.currentTimeMillis();
68 | // 判断是在当前时间窗口(限流开始时间)
69 | if (nowTime < requestTime.longValue() + TIME_REQUEST) {
70 | // 判断当前时间窗口请求内是否限流最大请求数
71 | if (requestNum.longValue() < MAX_NUM_REQUEST) {
72 | // 在时间窗口内且请求数量还没超过最大,请求数加一
73 | requestNum.incrementAndGet();
74 | logger.info("请求成功,当前请求是{}次", requestNum.intValue());
75 | return "请求成功,当前请求是" + requestNum.intValue() + "次";
76 | }
77 | } else {
78 | // 超时后重置(开启一个新的时间窗口)
79 | requestTime = new AtomicLong(nowTime);
80 | requestNum = new AtomicInteger(0);
81 | }
82 | logger.info("请求失败,被限流");
83 | return "请求失败,被限流";
84 | }
85 |
86 | /**
87 | * 计数器(固定时间窗口)请求接口(限流工具类实现)
88 | *
89 | * @param
90 | * @return java.lang.String
91 | * @throws
92 | * @author wliduo[i@dolyw.com]
93 | * @date 2019/11/25 18:02
94 | */
95 | @GetMapping("/redis")
96 | public String redis() {
97 | Long maxRequest = redisLimitUtil.limit(MAX_NUM_REQUEST.toString());
98 | // 结果请求数大于0说明不被限流
99 | if (maxRequest > 0) {
100 | logger.info("请求成功,当前请求是{}次", maxRequest);
101 | return "请求成功,当前请求是" + maxRequest + "次";
102 | }
103 | logger.info("请求失败,被限流");
104 | return "请求拥挤,请稍候重试";
105 | }
106 |
107 | /**
108 | * 计数器(固定时间窗口)请求接口(限流注解实现)
109 | *
110 | * @param
111 | * @return java.lang.String
112 | * @throws
113 | * @author wliduo[i@dolyw.com]
114 | * @date 2019/11/26 9:46
115 | */
116 | @Limit(maxRequest = "2", timeRequest = "3000")
117 | @GetMapping("/annotation")
118 | public String annotation() {
119 | logger.info("请求成功");
120 | return "请求成功";
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/main/java/com/example/controller/RedisLockController.java:
--------------------------------------------------------------------------------
1 | package com.example.controller;
2 |
3 | import com.example.exception.CustomException;
4 | import com.example.util.JedisUtil;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.web.bind.annotation.GetMapping;
8 | import org.springframework.web.bind.annotation.RequestMapping;
9 | import org.springframework.web.bind.annotation.RestController;
10 | import redis.clients.jedis.Jedis;
11 | import redis.clients.jedis.Transaction;
12 |
13 | import java.util.List;
14 | import java.util.concurrent.atomic.AtomicInteger;
15 |
16 | /**
17 | * Redis悲观锁和乐观锁测试
18 | *
19 | * @author wliduo[i@dolyw.com]
20 | * @date 2019/11/13 19:27
21 | */
22 | @RestController
23 | @RequestMapping("/redis")
24 | public class RedisLockController {
25 |
26 | /**
27 | * logger
28 | */
29 | private static final Logger logger = LoggerFactory.getLogger(RedisLockController.class);
30 |
31 | /**
32 | * 记录实际卖出的商品数量
33 | */
34 | private AtomicInteger successNum = new AtomicInteger(0);
35 |
36 | /**
37 | * 初始化库存数量缓存key
38 | */
39 | private static final String ITEM_STOCK = "item_stock";
40 |
41 | /**
42 | * 初始化库存数量
43 | */
44 | private static final String ITEM_STOCK_NUM = "30";
45 |
46 | /**
47 | * 获取卖出的商品数量
48 | *
49 | * @param
50 | * @return java.lang.String
51 | * @throws
52 | * @author wliduo[i@dolyw.com]
53 | * @date 2019/11/14 16:19
54 | */
55 | @GetMapping
56 | public String index() {
57 | return "卖出的商品数量: " + successNum.get() + "
Redis剩余的商品数量: " + JedisUtil.get(ITEM_STOCK);
58 | }
59 |
60 | /**
61 | * 初始化库存数量
62 | *
63 | * @param
64 | * @return java.lang.String
65 | * @throws
66 | * @author wliduo[i@dolyw.com]
67 | * @date 2019/11/14 16:56
68 | */
69 | @GetMapping("/init")
70 | public String init() {
71 | // 初始化库存数量
72 | JedisUtil.set(ITEM_STOCK, ITEM_STOCK_NUM);
73 | // 初始化实际卖出的商品数量0
74 | successNum.set(0);
75 | return "初始化库存成功";
76 | }
77 |
78 | /**
79 | * 会出现超卖情况的减少库存方式(典型的读后写,不可重复读)
80 | * https://www.jianshu.com/p/380ebb7c0847
81 | * 两个线程同时读取到库存为10,这样两线程计算写入后库存数值都为9,而卖出的数量为2
82 | * 就是超卖问题出现了,正常库存应该是8
83 | *
84 | * @param
85 | * @return java.lang.String
86 | * @throws
87 | * @author wliduo[i@dolyw.com]
88 | * @date 2019/11/14 17:01
89 | */
90 | @GetMapping(value = "/buy")
91 | public String buy() throws Exception {
92 | if (!JedisUtil.exists(ITEM_STOCK)) {
93 | throw new CustomException("库存Key在Redis不存在,请先初始化(缓存预热)");
94 | }
95 | Integer stock = Integer.parseInt(JedisUtil.get(ITEM_STOCK));
96 | // 读取数据后暂停10ms,出现问题的概率增大
97 | Thread.sleep(10);
98 | if (stock < 1) {
99 | return "库存不足";
100 | }
101 | stock = stock - 1;
102 | JedisUtil.set(ITEM_STOCK, stock.toString());
103 | return "减少库存成功,共减少" + successNum.incrementAndGet();
104 | }
105 |
106 | /**
107 | * 原子的减少库存方式(也会读后写,不可重复读,出现超卖问题)
108 | * https://www.jianshu.com/p/380ebb7c0847
109 | * 三个线程同时读取到库存为1时,这样两线程都穿过了if判断执行了decr操作
110 | * 而导致卖出数量多2份,且redis存储的库存为-2,原子操作导致减少库存都会执行
111 | *
112 | * @param
113 | * @return java.lang.String
114 | * @throws
115 | * @author wliduo[i@dolyw.com]
116 | * @date 2019/11/14 17:01
117 | */
118 | @GetMapping(value = "/buy2")
119 | public String buy2() throws Exception {
120 | if (!JedisUtil.exists(ITEM_STOCK)) {
121 | throw new CustomException("库存Key在Redis不存在,请先初始化(缓存预热)");
122 | }
123 | Integer stock = Integer.parseInt(JedisUtil.get(ITEM_STOCK));
124 | // 读取数据后暂停10ms,出现问题的概率增大
125 | Thread.sleep(10);
126 | if (stock < 1) {
127 | return "库存不足";
128 | }
129 | // 原子操作减一
130 | JedisUtil.decr(ITEM_STOCK);
131 | return "减少库存成功,共减少" + successNum.incrementAndGet();
132 | }
133 |
134 | /**
135 | * 添加事务的减少库存方式(乐观锁)
136 | *
137 | * @param
138 | * @return java.lang.String
139 | * @throws
140 | * @author wliduo[i@dolyw.com]
141 | * @date 2019/11/14 17:01
142 | */
143 | @GetMapping(value = "/buyTr")
144 | public String buyTr() {
145 | if (!JedisUtil.exists(ITEM_STOCK)) {
146 | throw new CustomException("库存Key在Redis不存在,请先初始化(缓存预热)");
147 | }
148 | Transaction transaction = null;
149 | try (Jedis jedis = JedisUtil.getJedis()) {
150 | // watch监视一个key,当事务执行之前这个key发生了改变,事务会被打断
151 | jedis.watch(ITEM_STOCK);
152 | Integer stock = Integer.parseInt(jedis.get(ITEM_STOCK));
153 | // 读取数据后暂停10ms,出现问题的概率增大
154 | Thread.sleep(10);
155 | if (stock > 0) {
156 | transaction = jedis.multi();
157 | stock = stock - 1;
158 | transaction.set(ITEM_STOCK, stock.toString());
159 | // 执行exec后就会自动执行jedis.unwatch()操作
160 | List