├── .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](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 4 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/dolyw/SeckillEvolution/pulls) 5 | [![GitHub stars](https://img.shields.io/github/stars/dolyw/SeckillEvolution.svg?style=social&label=Stars)](https://github.com/dolyw/SeckillEvolution) 6 | [![GitHub forks](https://img.shields.io/github/forks/dolyw/SeckillEvolution.svg?style=social&label=Fork)](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 result = transaction.exec(); 161 | if (result == null || result.isEmpty()) { 162 | // 可能是watch-key被外部修改,或者是数据操作被驳回 163 | System.out.println("Transaction error"); 164 | // watch-key被外部修改时,transaction.discard()操作会被自动触发 165 | return "Transaction error"; 166 | } 167 | } else { 168 | jedis.unwatch(); 169 | return "库存不足"; 170 | } 171 | return "减少库存成功,共减少" + successNum.incrementAndGet(); 172 | } catch (Exception e) { 173 | logger.error(e.getMessage()); 174 | if (transaction != null) { 175 | transaction.discard(); 176 | } 177 | return "fail"; 178 | } 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/com/example/controller/SeckillEvolutionController.java: -------------------------------------------------------------------------------- 1 | package com.example.controller; 2 | 3 | import com.example.common.ResponseBean; 4 | import com.example.constant.Constant; 5 | import com.example.dto.custom.StockDto; 6 | import com.example.dto.custom.StockOrderDto; 7 | import com.example.exception.CustomException; 8 | import com.example.limit.Limit; 9 | import com.example.seckill.ISeckillService; 10 | import com.example.service.ISeckillEvolutionService; 11 | import com.example.service.IStockOrderService; 12 | import com.example.service.IStockService; 13 | import com.example.util.JedisUtil; 14 | import com.example.util.RedisLimitUtil; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.beans.factory.annotation.Qualifier; 19 | import org.springframework.http.HttpStatus; 20 | import org.springframework.web.bind.annotation.*; 21 | 22 | /** 23 | * 一个简单的秒杀架构的演变 24 | * 25 | * @author wliduo[i@dolyw.com] 26 | * @date 2019/11/20 19:49 27 | */ 28 | @RestController 29 | @RequestMapping("/seckill") 30 | public class SeckillEvolutionController { 31 | 32 | /** 33 | * logger 34 | */ 35 | private static final Logger logger = LoggerFactory.getLogger(SeckillEvolutionController.class); 36 | 37 | private final IStockService stockService; 38 | 39 | private final IStockOrderService stockOrderService; 40 | 41 | private final ISeckillEvolutionService seckillEvolutionService; 42 | 43 | /** 44 | * 构造注入 45 | * @param stockService 46 | * @param stockOrderService 47 | */ 48 | @Autowired 49 | public SeckillEvolutionController(IStockService stockService, IStockOrderService stockOrderService, 50 | ISeckillEvolutionService seckillEvolutionService) { 51 | this.stockService = stockService; 52 | this.stockOrderService = stockOrderService; 53 | this.seckillEvolutionService = seckillEvolutionService; 54 | } 55 | 56 | /** 57 | * 初始化库存数量 58 | */ 59 | private static final Integer ITEM_STOCK_COUNT = 10; 60 | 61 | /** 62 | * 初始化卖出数量,乐观锁版本 63 | */ 64 | private static final Integer ITEM_STOCK_SALE = 0; 65 | 66 | /** 67 | * RedisLimitUtil 68 | */ 69 | @Autowired 70 | private RedisLimitUtil redisLimitUtil; 71 | 72 | /** 73 | * 初始化库存数量 74 | * 75 | * @param id 商品ID 76 | * @return com.example.common.ResponseBean 77 | * @throws 78 | * @author wliduo[i@dolyw.com] 79 | * @date 2019/11/22 15:59 80 | */ 81 | @PutMapping("/init/{id}") 82 | public ResponseBean init(@PathVariable("id") Integer id) { 83 | // 更新库存表该商品的库存,已售,乐观锁版本号 84 | StockDto stockDto = new StockDto(); 85 | stockDto.setId(id); 86 | stockDto.setName(Constant.ITEM_STOCK_NAME); 87 | stockDto.setCount(ITEM_STOCK_COUNT); 88 | stockDto.setSale(ITEM_STOCK_SALE); 89 | stockDto.setVersion(ITEM_STOCK_SALE); 90 | stockService.updateByPrimaryKey(stockDto); 91 | // 删除订单表该商品所有数据 92 | StockOrderDto stockOrderDto = new StockOrderDto(); 93 | stockOrderDto.setStockId(id); 94 | stockOrderService.delete(stockOrderDto); 95 | return new ResponseBean(HttpStatus.OK.value(), "初始化库存成功", null); 96 | } 97 | 98 | /** 99 | * 缓存预热 100 | * 101 | * @param id 商品ID 102 | * @return com.example.common.ResponseBean 103 | * @throws 104 | * @author wliduo[i@dolyw.com] 105 | * @date 2019/11/22 15:59 106 | */ 107 | @PutMapping("/initCache/{id}") 108 | public ResponseBean initCache(@PathVariable("id") Integer id) { 109 | StockDto stockDto = stockService.selectByPrimaryKey(id); 110 | // 商品缓存预热 111 | JedisUtil.set(Constant.PREFIX_COUNT + id.toString(), stockDto.getCount().toString()); 112 | JedisUtil.set(Constant.PREFIX_SALE + id.toString(), stockDto.getSale().toString()); 113 | JedisUtil.set(Constant.PREFIX_VERSION + id.toString(), stockDto.getVersion().toString()); 114 | return new ResponseBean(HttpStatus.OK.value(), "缓存预热成功", null); 115 | } 116 | 117 | /** 118 | * 传统方式下订单 119 | * 120 | * @param id 商品ID 121 | * @return com.example.common.ResponseBean 122 | * @throws Exception 123 | * @author wliduo[i@dolyw.com] 124 | * @date 2019/11/21 19:50 125 | */ 126 | @PostMapping("/createWrongOrder/{id}") 127 | public ResponseBean createWrongOrder(@PathVariable("id") Integer id) throws Exception { 128 | Integer orderCount = seckillEvolutionService.createWrongOrder(id); 129 | return new ResponseBean(HttpStatus.OK.value(), "购买成功", orderCount); 130 | } 131 | 132 | /** 133 | * 使用乐观锁下订单 134 | * 135 | * @param id 商品ID 136 | * @return com.example.common.ResponseBean 137 | * @throws Exception 138 | * @author wliduo[i@dolyw.com] 139 | * @date 2019/11/22 14:24 140 | */ 141 | @PostMapping("/createOptimisticLockOrder/{id}") 142 | public ResponseBean createOptimisticLockOrder(@PathVariable("id") Integer id) throws Exception { 143 | Integer orderCount = seckillEvolutionService.createOptimisticLockOrder(id); 144 | return new ResponseBean(HttpStatus.OK.value(), "购买成功", orderCount); 145 | } 146 | 147 | /** 148 | * 使用乐观锁下订单,并且添加读缓存,性能提升 149 | * 150 | * @param id 商品ID 151 | * @return com.example.common.ResponseBean 152 | * @throws Exception 153 | * @author wliduo[i@dolyw.com] 154 | * @date 2019/11/22 14:24 155 | */ 156 | @PostMapping("/createOptimisticLockOrderWithRedis/{id}") 157 | public ResponseBean createOptimisticLockOrderWithRedis(@PathVariable("id") Integer id) throws Exception { 158 | // 错误的,线程不安全 159 | // Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisWrong(id); 160 | // 正确的,线程安全 161 | Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisSafe(id); 162 | return new ResponseBean(HttpStatus.OK.value(), "购买成功", null); 163 | } 164 | 165 | /** 166 | * 使用乐观锁下订单,并且添加读缓存,再添加限流 167 | * 168 | * @param id 商品ID 169 | * @return com.example.common.ResponseBean 170 | * @throws Exception 171 | * @author wliduo[i@dolyw.com] 172 | * @date 2019/11/22 14:24 173 | */ 174 | @Limit 175 | @PostMapping("/createOptimisticLockOrderWithRedisLimit/{id}") 176 | public ResponseBean createOptimisticLockOrderWithRedisLimit(@PathVariable("id") Integer id) throws Exception { 177 | // 错误的,线程不安全 178 | // Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisWrong(id); 179 | // 正确的,线程安全 180 | Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisSafe(id); 181 | return new ResponseBean(HttpStatus.OK.value(), "购买成功", null); 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/com/example/controller/StockController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.controller; 7 | 8 | import org.springframework.web.bind.annotation.*; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import com.example.dto.custom.StockDto; 11 | import com.example.service.IStockService; 12 | import com.example.common.ResponseBean; 13 | import com.example.exception.CustomException; 14 | 15 | import com.github.pagehelper.PageHelper; 16 | import com.github.pagehelper.PageInfo; 17 | import org.springframework.http.HttpStatus; 18 | import org.springframework.web.bind.annotation.*; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | /** 26 | * StockController 27 | * @author wliduo[i@dolyw.com] 28 | * @date 2019-11-20 18:03:33 29 | */ 30 | @RestController 31 | @RequestMapping("stock") 32 | public class StockController { 33 | 34 | private final IStockService stockService; 35 | 36 | @Autowired 37 | public StockController (IStockService stockService) { 38 | this.stockService = stockService; 39 | } 40 | 41 | /** 42 | * 列表 43 | * @author wliduo[i@dolyw.com] 44 | * @date 2019-11-20 18:03:33 45 | */ 46 | @GetMapping 47 | public ResponseBean list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer rows) { 48 | if (page <= 0 || rows <= 0) { 49 | page = 1; 50 | rows = 10; 51 | } 52 | PageHelper.startPage(page, rows); 53 | List list = stockService.selectAll(); 54 | if (list == null || list.size() <= 0) { 55 | throw new CustomException("查询失败(Query Failure)"); 56 | } 57 | PageInfo pageInfo = new PageInfo(list); 58 | Map result = new HashMap(16); 59 | result.put("count", pageInfo.getTotal()); 60 | result.put("data", pageInfo.getList()); 61 | return new ResponseBean(HttpStatus.OK.value(), "查询成功(Query was successful)", result); 62 | } 63 | 64 | /** 65 | * 查询 66 | * @author wliduo[i@dolyw.com] 67 | * @date 2019-11-20 18:03:33 68 | */ 69 | @GetMapping("/{id}") 70 | public ResponseBean findById(@PathVariable("id") Integer id) { 71 | StockDto stockDto = stockService.selectByPrimaryKey(id); 72 | if (stockDto == null) { 73 | throw new CustomException("查询失败(Query Failure)"); 74 | } 75 | return new ResponseBean(HttpStatus.OK.value(), "查询成功(Query was successful)", stockDto); 76 | } 77 | 78 | /** 79 | * 新增 80 | * @author wliduo[i@dolyw.com] 81 | * @date 2019-11-20 18:03:33 82 | */ 83 | @PostMapping 84 | public ResponseBean add(@RequestBody StockDto stockDto) { 85 | int count = stockService.insert(stockDto); 86 | // 操作数量小于0 87 | if (count <= 0) { 88 | throw new CustomException("新增失败(Insert Failure)"); 89 | } 90 | return new ResponseBean(HttpStatus.OK.value(), "新增成功(Insert Success)", stockDto); 91 | } 92 | 93 | /** 94 | * 更新 95 | * @author wliduo[i@dolyw.com] 96 | * @date 2019-11-20 18:03:33 97 | */ 98 | @PutMapping 99 | public ResponseBean update(@RequestBody StockDto stockDto) { 100 | int count = stockService.updateByPrimaryKeySelective(stockDto); 101 | // 操作数量小于0 102 | if (count <= 0) { 103 | throw new CustomException("更新失败(Update Failure)"); 104 | } 105 | return new ResponseBean(HttpStatus.OK.value(), "更新成功(Update Success)", stockDto); 106 | } 107 | 108 | /** 109 | * 删除 110 | * @author wliduo[i@dolyw.com] 111 | * @date 2019-11-20 18:03:33 112 | */ 113 | @DeleteMapping("/{id}") 114 | public ResponseBean delete(@PathVariable("id") Integer id) { 115 | int count = stockService.deleteByPrimaryKey(id); 116 | // 操作数量小于0 117 | if (count <= 0) { 118 | throw new CustomException("删除失败,ID不存在(Deletion Failed. ID does not exist.)"); 119 | } 120 | return new ResponseBean(HttpStatus.OK.value(), "删除成功(Delete Success)", null); 121 | } 122 | 123 | } -------------------------------------------------------------------------------- /src/main/java/com/example/controller/StockOrderController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.controller; 7 | 8 | import org.springframework.web.bind.annotation.*; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import com.example.dto.custom.StockOrderDto; 11 | import com.example.service.IStockOrderService; 12 | import com.example.common.ResponseBean; 13 | import com.example.exception.CustomException; 14 | 15 | import com.github.pagehelper.PageHelper; 16 | import com.github.pagehelper.PageInfo; 17 | import org.springframework.http.HttpStatus; 18 | import org.springframework.web.bind.annotation.*; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | /** 26 | * StockOrderController 27 | * @author wliduo[i@dolyw.com] 28 | * @date 2019-11-20 18:03:33 29 | */ 30 | @RestController 31 | @RequestMapping("stockOrder") 32 | public class StockOrderController { 33 | 34 | private final IStockOrderService stockOrderService; 35 | 36 | @Autowired 37 | public StockOrderController (IStockOrderService stockOrderService) { 38 | this.stockOrderService = stockOrderService; 39 | } 40 | 41 | /** 42 | * 列表 43 | * @author wliduo[i@dolyw.com] 44 | * @date 2019-11-20 18:03:33 45 | */ 46 | @GetMapping 47 | public ResponseBean list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer rows) { 48 | if (page <= 0 || rows <= 0) { 49 | page = 1; 50 | rows = 10; 51 | } 52 | PageHelper.startPage(page, rows); 53 | List list = stockOrderService.selectAll(); 54 | if (list == null || list.size() <= 0) { 55 | throw new CustomException("查询失败(Query Failure)"); 56 | } 57 | PageInfo pageInfo = new PageInfo(list); 58 | Map result = new HashMap(16); 59 | result.put("count", pageInfo.getTotal()); 60 | result.put("data", pageInfo.getList()); 61 | return new ResponseBean(HttpStatus.OK.value(), "查询成功(Query was successful)", result); 62 | } 63 | 64 | /** 65 | * 查询 66 | * @author wliduo[i@dolyw.com] 67 | * @date 2019-11-20 18:03:33 68 | */ 69 | @GetMapping("/{id}") 70 | public ResponseBean findById(@PathVariable("id") Integer id) { 71 | StockOrderDto stockOrderDto = stockOrderService.selectByPrimaryKey(id); 72 | if (stockOrderDto == null) { 73 | throw new CustomException("查询失败(Query Failure)"); 74 | } 75 | return new ResponseBean(HttpStatus.OK.value(), "查询成功(Query was successful)", stockOrderDto); 76 | } 77 | 78 | /** 79 | * 新增 80 | * @author wliduo[i@dolyw.com] 81 | * @date 2019-11-20 18:03:33 82 | */ 83 | @PostMapping 84 | public ResponseBean add(@RequestBody StockOrderDto stockOrderDto) { 85 | int count = stockOrderService.insert(stockOrderDto); 86 | // 操作数量小于0 87 | if (count <= 0) { 88 | throw new CustomException("新增失败(Insert Failure)"); 89 | } 90 | return new ResponseBean(HttpStatus.OK.value(), "新增成功(Insert Success)", stockOrderDto); 91 | } 92 | 93 | /** 94 | * 更新 95 | * @author wliduo[i@dolyw.com] 96 | * @date 2019-11-20 18:03:33 97 | */ 98 | @PutMapping 99 | public ResponseBean update(@RequestBody StockOrderDto stockOrderDto) { 100 | int count = stockOrderService.updateByPrimaryKeySelective(stockOrderDto); 101 | // 操作数量小于0 102 | if (count <= 0) { 103 | throw new CustomException("更新失败(Update Failure)"); 104 | } 105 | return new ResponseBean(HttpStatus.OK.value(), "更新成功(Update Success)", stockOrderDto); 106 | } 107 | 108 | /** 109 | * 删除 110 | * @author wliduo[i@dolyw.com] 111 | * @date 2019-11-20 18:03:33 112 | */ 113 | @DeleteMapping("/{id}") 114 | public ResponseBean delete(@PathVariable("id") Integer id) { 115 | int count = stockOrderService.deleteByPrimaryKey(id); 116 | // 操作数量小于0 117 | if (count <= 0) { 118 | throw new CustomException("删除失败,ID不存在(Deletion Failed. ID does not exist.)"); 119 | } 120 | return new ResponseBean(HttpStatus.OK.value(), "删除成功(Delete Success)", null); 121 | } 122 | 123 | } -------------------------------------------------------------------------------- /src/main/java/com/example/dao/StockDao.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.dao; 7 | 8 | import com.example.dto.custom.StockDto; 9 | import org.apache.ibatis.annotations.Update; 10 | import org.springframework.stereotype.Repository; 11 | import tk.mybatis.mapper.common.Mapper; 12 | import java.util.List; 13 | 14 | /** 15 | * StockDao 16 | * @author wliduo[i@dolyw.com] 17 | * @date 2019-11-20 18:03:33 18 | */ 19 | @Repository 20 | public interface StockDao extends Mapper { 21 | 22 | /** 23 | * 列表 24 | * @param stockDto 25 | * @return java.util.List 26 | * @author wliduo[i@dolyw.com] 27 | * @date 2019-11-20 18:03:33 28 | */ 29 | public List findPageInfo(StockDto stockDto); 30 | 31 | /** 32 | * 乐观锁更新扣减库存 33 | * 34 | * @param stockDto 35 | * @return int 36 | * @throws 37 | * @author wliduo[i@dolyw.com] 38 | * @date 2019/11/22 14:14 39 | */ 40 | @Update("UPDATE t_seckill_stock SET count = count - 1, sale = sale + 1, version = version + 1 " + 41 | "WHERE id = #{id, jdbcType = INTEGER} AND version = #{version, jdbcType = INTEGER} " + 42 | "") 43 | int updateByOptimisticLock(StockDto stockDto); 44 | 45 | } -------------------------------------------------------------------------------- /src/main/java/com/example/dao/StockOrderDao.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.dao; 7 | 8 | import com.example.dto.custom.StockOrderDto; 9 | import org.springframework.stereotype.Repository; 10 | import tk.mybatis.mapper.common.Mapper; 11 | import java.util.List; 12 | 13 | /** 14 | * StockOrderDao 15 | * @author wliduo[i@dolyw.com] 16 | * @date 2019-11-20 18:03:33 17 | */ 18 | @Repository 19 | public interface StockOrderDao extends Mapper { 20 | 21 | /** 22 | * 列表 23 | * @param stockOrderDto 24 | * @return java.util.List 25 | * @author wliduo[i@dolyw.com] 26 | * @date 2019-11-20 18:03:33 27 | */ 28 | public List findPageInfo(StockOrderDto stockOrderDto); 29 | } -------------------------------------------------------------------------------- /src/main/java/com/example/dto/custom/StockDto.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.dto.custom; 7 | 8 | import java.io.Serializable; 9 | import javax.persistence.Table; 10 | import com.example.dto.domain.StockDtoBase; 11 | 12 | /** 13 | * StockDto 14 | * @author wliduo[i@dolyw.com] 15 | * @date 2019-11-20 18:03:33 16 | */ 17 | @Table(name = "t_seckill_stock") 18 | public class StockDto extends StockDtoBase implements Serializable { 19 | 20 | private static final long serialVersionUID = StockDto.class.getName().hashCode(); 21 | 22 | } -------------------------------------------------------------------------------- /src/main/java/com/example/dto/custom/StockOrderDto.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.dto.custom; 7 | 8 | import java.io.Serializable; 9 | import javax.persistence.Table; 10 | import com.example.dto.domain.StockOrderDtoBase; 11 | 12 | /** 13 | * StockOrderDto 14 | * @author wliduo[i@dolyw.com] 15 | * @date 2019-11-20 18:03:33 16 | */ 17 | @Table(name = "t_seckill_stock_order") 18 | public class StockOrderDto extends StockOrderDtoBase implements Serializable { 19 | 20 | private static final long serialVersionUID = StockOrderDto.class.getName().hashCode(); 21 | 22 | } -------------------------------------------------------------------------------- /src/main/java/com/example/dto/domain/StockDtoBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.dto.domain; 7 | 8 | import java.io.Serializable; 9 | import java.util.Date; 10 | 11 | import javax.persistence.GeneratedValue; 12 | import javax.persistence.GenerationType; 13 | import javax.persistence.Id; 14 | 15 | import org.apache.commons.lang3.builder.EqualsBuilder; 16 | import org.apache.commons.lang3.builder.HashCodeBuilder; 17 | import org.apache.commons.lang3.builder.ToStringBuilder; 18 | import org.apache.commons.lang3.builder.ToStringStyle; 19 | 20 | import com.fasterxml.jackson.annotation.JsonFormat; 21 | 22 | /** 23 | * StockDtoBase 24 | * @author wliduo[i@dolyw.com] 25 | * @date 2019-11-20 18:03:33 26 | */ 27 | public class StockDtoBase implements Serializable { 28 | 29 | private static final long serialVersionUID = StockDtoBase.class.getName().hashCode(); 30 | 31 | /** 库存ID */ 32 | @Id 33 | @GeneratedValue(strategy = GenerationType.IDENTITY) 34 | private Integer id; 35 | 36 | /** 名称 */ 37 | private String name; 38 | 39 | /** 库存 */ 40 | private Integer count; 41 | 42 | /** 已售 */ 43 | private Integer sale; 44 | 45 | /** 乐观锁,版本号 */ 46 | private Integer version; 47 | 48 | /** 创建时间 */ 49 | @JsonFormat(pattern="yyyy-MM-dd",timezone = "GMT+8") 50 | private Date createTime; 51 | 52 | /** 更新时间 */ 53 | @JsonFormat(pattern="yyyy-MM-dd",timezone = "GMT+8") 54 | private Date updateTime; 55 | 56 | /** 57 | * 获取属性库存ID的值 58 | */ 59 | public Integer getId() { 60 | return this.id; 61 | } 62 | 63 | /** 64 | * 设置属性库存ID的值 65 | */ 66 | public void setId(Integer id) { 67 | this.id = id; 68 | } 69 | 70 | /** 71 | * 获取属性名称的值 72 | */ 73 | public String getName() { 74 | return this.name; 75 | } 76 | 77 | /** 78 | * 设置属性名称的值 79 | */ 80 | public void setName(String name) { 81 | this.name = name; 82 | } 83 | 84 | /** 85 | * 获取属性库存的值 86 | */ 87 | public Integer getCount() { 88 | return this.count; 89 | } 90 | 91 | /** 92 | * 设置属性库存的值 93 | */ 94 | public void setCount(Integer count) { 95 | this.count = count; 96 | } 97 | 98 | /** 99 | * 获取属性已售的值 100 | */ 101 | public Integer getSale() { 102 | return this.sale; 103 | } 104 | 105 | /** 106 | * 设置属性已售的值 107 | */ 108 | public void setSale(Integer sale) { 109 | this.sale = sale; 110 | } 111 | 112 | /** 113 | * 获取属性乐观锁,版本号的值 114 | */ 115 | public Integer getVersion() { 116 | return this.version; 117 | } 118 | 119 | /** 120 | * 设置属性乐观锁,版本号的值 121 | */ 122 | public void setVersion(Integer version) { 123 | this.version = version; 124 | } 125 | 126 | /** 127 | * 获取属性创建时间的值 128 | */ 129 | public Date getCreateTime() { 130 | return this.createTime; 131 | } 132 | 133 | /** 134 | * 设置属性创建时间的值 135 | */ 136 | public void setCreateTime(Date createTime) { 137 | this.createTime = createTime; 138 | } 139 | 140 | /** 141 | * 获取属性更新时间的值 142 | */ 143 | public Date getUpdateTime() { 144 | return this.updateTime; 145 | } 146 | 147 | /** 148 | * 设置属性更新时间的值 149 | */ 150 | public void setUpdateTime(Date updateTime) { 151 | this.updateTime = updateTime; 152 | } 153 | 154 | @Override 155 | public boolean equals(Object obj) { 156 | return EqualsBuilder.reflectionEquals(this, obj); 157 | } 158 | 159 | @Override 160 | public int hashCode() { 161 | return HashCodeBuilder.reflectionHashCode(this); 162 | } 163 | 164 | @Override 165 | public String toString() { 166 | return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); 167 | } 168 | 169 | } -------------------------------------------------------------------------------- /src/main/java/com/example/dto/domain/StockOrderDtoBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.dto.domain; 7 | 8 | import java.io.Serializable; 9 | import java.util.Date; 10 | 11 | import javax.persistence.GeneratedValue; 12 | import javax.persistence.GenerationType; 13 | import javax.persistence.Id; 14 | 15 | import org.apache.commons.lang3.builder.EqualsBuilder; 16 | import org.apache.commons.lang3.builder.HashCodeBuilder; 17 | import org.apache.commons.lang3.builder.ToStringBuilder; 18 | import org.apache.commons.lang3.builder.ToStringStyle; 19 | 20 | import com.fasterxml.jackson.annotation.JsonFormat; 21 | 22 | /** 23 | * StockOrderDtoBase 24 | * @author wliduo[i@dolyw.com] 25 | * @date 2019-11-20 18:03:33 26 | */ 27 | public class StockOrderDtoBase implements Serializable { 28 | 29 | private static final long serialVersionUID = StockOrderDtoBase.class.getName().hashCode(); 30 | 31 | /** id */ 32 | @Id 33 | @GeneratedValue(strategy = GenerationType.IDENTITY) 34 | private Integer id; 35 | 36 | /** 库存ID */ 37 | private Integer stockId; 38 | 39 | /** 商品名称 */ 40 | private String name; 41 | 42 | /** 创建时间 */ 43 | @JsonFormat(pattern="yyyy-MM-dd",timezone = "GMT+8") 44 | private Date createTime; 45 | 46 | /** 47 | * 获取属性id的值 48 | */ 49 | public Integer getId() { 50 | return this.id; 51 | } 52 | 53 | /** 54 | * 设置属性id的值 55 | */ 56 | public void setId(Integer id) { 57 | this.id = id; 58 | } 59 | 60 | /** 61 | * 获取属性库存ID的值 62 | */ 63 | public Integer getStockId() { 64 | return this.stockId; 65 | } 66 | 67 | /** 68 | * 设置属性库存ID的值 69 | */ 70 | public void setStockId(Integer stockId) { 71 | this.stockId = stockId; 72 | } 73 | 74 | /** 75 | * 获取属性商品名称的值 76 | */ 77 | public String getName() { 78 | return this.name; 79 | } 80 | 81 | /** 82 | * 设置属性商品名称的值 83 | */ 84 | public void setName(String name) { 85 | this.name = name; 86 | } 87 | 88 | /** 89 | * 获取属性创建时间的值 90 | */ 91 | public Date getCreateTime() { 92 | return this.createTime; 93 | } 94 | 95 | /** 96 | * 设置属性创建时间的值 97 | */ 98 | public void setCreateTime(Date createTime) { 99 | this.createTime = createTime; 100 | } 101 | 102 | @Override 103 | public boolean equals(Object obj) { 104 | return EqualsBuilder.reflectionEquals(this, obj); 105 | } 106 | 107 | @Override 108 | public int hashCode() { 109 | return HashCodeBuilder.reflectionHashCode(this); 110 | } 111 | 112 | @Override 113 | public String toString() { 114 | return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); 115 | } 116 | 117 | } -------------------------------------------------------------------------------- /src/main/java/com/example/exception/CustomException.java: -------------------------------------------------------------------------------- 1 | package com.example.exception; 2 | 3 | /** 4 | * 自定义异常(CustomException) 5 | * 6 | * @author wliduo[i@dolyw.com] 7 | * @date 2018/8/30 13:59 8 | */ 9 | public class CustomException extends RuntimeException { 10 | 11 | public CustomException(String msg){ 12 | super(msg); 13 | } 14 | 15 | public CustomException() { 16 | super(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/example/exception/SystemException.java: -------------------------------------------------------------------------------- 1 | package com.example.exception; 2 | 3 | /** 4 | * 系统异常(SystemException) 5 | * 6 | * @author wliduo[i@dolyw.com] 7 | * @date 2018/8/30 13:59 8 | */ 9 | public class SystemException extends RuntimeException { 10 | 11 | public SystemException(String msg){ 12 | super(msg); 13 | } 14 | 15 | public SystemException() { 16 | super(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/example/limit/Limit.java: -------------------------------------------------------------------------------- 1 | package com.example.limit; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 限流注解 7 | * 8 | * @author wliduo[i@dolyw.com] 9 | * @date 2019/11/26 9:59 10 | */ 11 | @Documented 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target(ElementType.METHOD) 14 | public @interface Limit { 15 | 16 | /** 17 | * 限流最大请求数 18 | * @return 19 | */ 20 | String maxRequest() default "10"; 21 | 22 | /** 23 | * 一个时间窗口(毫秒) 24 | * @return 25 | */ 26 | String timeRequest() default "1000"; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/example/limit/LimitAspect.java: -------------------------------------------------------------------------------- 1 | package com.example.limit; 2 | 3 | import com.example.exception.CustomException; 4 | import com.example.util.RedisLimitUtil; 5 | import org.aspectj.lang.ProceedingJoinPoint; 6 | import org.aspectj.lang.annotation.*; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.core.annotation.Order; 11 | import org.springframework.stereotype.Component; 12 | 13 | /** 14 | * LimitAspect限流切面 15 | * 16 | * @author wliduo[i@dolyw.com] 17 | * @date 2019/11/26 10:07 18 | */ 19 | @Order(0) 20 | @Aspect 21 | @Component 22 | public class LimitAspect { 23 | 24 | /** 25 | * logger 26 | */ 27 | private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class); 28 | 29 | /** 30 | * 一个时间窗口时间(毫秒)(限流时间) 31 | */ 32 | private static final String TIME_REQUEST = "1000"; 33 | 34 | /** 35 | * RedisLimitUtil 36 | */ 37 | @Autowired 38 | private RedisLimitUtil redisLimitUtil; 39 | 40 | /** 41 | * 对应注解 42 | * 43 | * @param 44 | * @return void 45 | * @throws 46 | * @author wliduo[i@dolyw.com] 47 | * @date 2019/11/26 10:11 48 | */ 49 | @Pointcut("@annotation(com.example.limit.Limit)") 50 | public void aspect() {} 51 | 52 | /** 53 | * 切面 54 | * 55 | * @param proceedingJoinPoint 56 | * @return java.lang.Object 57 | * @throws 58 | * @author wliduo[i@dolyw.com] 59 | * @date 2019/11/26 10:11 60 | */ 61 | @Around("aspect() && @annotation(limit)") 62 | public Object Interceptor(ProceedingJoinPoint proceedingJoinPoint, Limit limit) { 63 | Object result = null; 64 | Long maxRequest = 0L; 65 | // 一个时间窗口(毫秒)为1000的话默认调用秒级限流判断(每秒限制多少请求) 66 | if (TIME_REQUEST.equals(limit.timeRequest())) { 67 | maxRequest = redisLimitUtil.limit(limit.maxRequest()); 68 | } else { 69 | maxRequest = redisLimitUtil.limit(limit.maxRequest(), limit.timeRequest()); 70 | } 71 | // 返回请求数量大于0说明不被限流 72 | if (maxRequest > 0) { 73 | // 放行,执行后续方法 74 | try { 75 | result = proceedingJoinPoint.proceed(); 76 | } catch (Throwable throwable) { 77 | throw new CustomException(throwable.getMessage()); 78 | } 79 | } else { 80 | // 直接返回响应结果 81 | throw new CustomException("请求拥挤,请稍候重试"); 82 | } 83 | return result; 84 | } 85 | 86 | /** 87 | * 执行方法前再执行 88 | * 89 | * @param limit 90 | * @return void 91 | * @throws 92 | * @author wliduo[i@dolyw.com] 93 | * @date 2019/11/26 10:10 94 | */ 95 | @Before("aspect() && @annotation(limit)") 96 | public void before(Limit limit) { 97 | // logger.info("before"); 98 | } 99 | 100 | /** 101 | * 执行方法后再执行 102 | * 103 | * @param limit 104 | * @return void 105 | * @throws 106 | * @author wliduo[i@dolyw.com] 107 | * @date 2019/11/26 10:10 108 | */ 109 | @After("aspect() && @annotation(limit)") 110 | public void after(Limit limit) { 111 | // logger.info("after"); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/example/seckill/ISeckillService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.seckill; 7 | 8 | import com.example.dto.custom.StockDto; 9 | 10 | /** 11 | * 统一接口 12 | * 13 | * @author wliduo[i@dolyw.com] 14 | * @date 2019-11-20 18:03:33 15 | */ 16 | public interface ISeckillService { 17 | 18 | /** 19 | * 检查库存 20 | * 21 | * @param id 22 | * @return com.example.dto.custom.StockDto 23 | * @throws 24 | * @author wliduo[i@dolyw.com] 25 | * @date 2019/11/20 20:22 26 | */ 27 | StockDto checkStock(Integer id) throws Exception; 28 | 29 | /** 30 | * 扣库存 31 | * 32 | * @param stockDto 33 | * @return java.lang.Integer 操作成功条数 34 | * @throws 35 | * @author wliduo[i@dolyw.com] 36 | * @date 2019/11/20 20:24 37 | */ 38 | Integer saleStock(StockDto stockDto) throws Exception; 39 | 40 | /** 41 | * 下订单 42 | * 43 | * @param stockDto 44 | * @return java.lang.Integer 操作成功条数 45 | * @throws 46 | * @author wliduo[i@dolyw.com] 47 | * @date 2019/11/20 20:26 48 | */ 49 | Integer createOrder(StockDto stockDto); 50 | 51 | } -------------------------------------------------------------------------------- /src/main/java/com/example/seckill/impl/SeckillOptimisticLockRedisSafeServiceImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.seckill.impl; 7 | 8 | 9 | import com.example.constant.Constant; 10 | import com.example.dao.StockDao; 11 | import com.example.dao.StockOrderDao; 12 | import com.example.dto.custom.StockDto; 13 | import com.example.dto.custom.StockOrderDto; 14 | import com.example.exception.CustomException; 15 | import com.example.seckill.ISeckillService; 16 | import com.example.util.JedisUtil; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.stereotype.Service; 21 | 22 | import java.util.List; 23 | 24 | /** 25 | * 使用乐观锁加缓存的方式(线程安全)(使用Redis批量操作mget和mset具有原子性) 26 | * 27 | * @author wliduo[i@dolyw.com] 28 | * @date 2019-11-20 18:03:33 29 | */ 30 | @Service("seckillOptimisticLockRedisSafeService") 31 | public class SeckillOptimisticLockRedisSafeServiceImpl implements ISeckillService { 32 | 33 | /** 34 | * logger 35 | */ 36 | private static final Logger logger = LoggerFactory.getLogger(SeckillOptimisticLockRedisSafeServiceImpl.class); 37 | 38 | @Autowired 39 | private StockDao stockDao; 40 | 41 | @Autowired 42 | private StockOrderDao stockOrderDao; 43 | 44 | @Override 45 | public StockDto checkStock(Integer id) throws Exception { 46 | // 使用缓存读取库存,减轻DB压力,Redis批量操作(具有原子性)解决线程安全问题 47 | List dataList = JedisUtil.mget(Constant.PREFIX_COUNT + id, 48 | Constant.PREFIX_SALE + id, Constant.PREFIX_VERSION + id); 49 | Integer count = Integer.parseInt(dataList.get(0)); 50 | Integer sale = Integer.parseInt(dataList.get(1)); 51 | Integer version = Integer.parseInt(dataList.get(2)); 52 | if (count > 0) { 53 | // 还有库存 54 | StockDto stockDto = new StockDto(); 55 | stockDto.setId(id); 56 | stockDto.setCount(count); 57 | stockDto.setSale(sale); 58 | stockDto.setVersion(version); 59 | Thread.sleep(10); 60 | return stockDto; 61 | } 62 | throw new CustomException("库存不足"); 63 | } 64 | 65 | @Override 66 | public Integer saleStock(StockDto stockDto) throws Exception { 67 | Integer saleCount = stockDao.updateByOptimisticLock(stockDto); 68 | // 操作数据大于0,说明扣库存成功 69 | if (saleCount > 0) { 70 | logger.info("版本号:{} {} {}", stockDto.getCount(), stockDto.getSale(), stockDto.getVersion()); 71 | Thread.sleep(10); 72 | // 更新缓存,这里更新需要保证三个数据(库存,已售,乐观锁版本号)的一致性,使用mset原子操作 73 | updateCache(stockDto); 74 | } 75 | return saleCount; 76 | } 77 | 78 | /** 79 | * 这里遵循先更新数据库,再更新缓存,详细的数据库与缓存一致性解析可以查看 80 | * https://note.dolyw.com/cache/00-DataBaseConsistency.html 81 | * 82 | * @param stockDto 83 | * @return java.lang.Integer 84 | * @throws 85 | * @author wliduo[i@dolyw.com] 86 | * @date 2019/11/22 16:25 87 | */ 88 | public void updateCache(StockDto stockDto) { 89 | Integer count = stockDto.getCount() - 1; 90 | Integer sale = stockDto.getSale() + 1; 91 | Integer version = stockDto.getVersion() + 1; 92 | JedisUtil.mset(Constant.PREFIX_COUNT + stockDto.getId(), count.toString(), 93 | Constant.PREFIX_SALE + stockDto.getId(), sale.toString(), 94 | Constant.PREFIX_VERSION + stockDto.getId(), version.toString()); 95 | } 96 | 97 | @Override 98 | public Integer createOrder(StockDto stockDto) { 99 | StockOrderDto stockOrderDto = new StockOrderDto(); 100 | stockOrderDto.setStockId(stockDto.getId()); 101 | return stockOrderDao.insertSelective(stockOrderDto); 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /src/main/java/com/example/seckill/impl/SeckillOptimisticLockRedisWrongServiceImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.seckill.impl; 7 | 8 | 9 | import com.example.constant.Constant; 10 | import com.example.dao.StockDao; 11 | import com.example.dao.StockOrderDao; 12 | import com.example.dto.custom.StockDto; 13 | import com.example.dto.custom.StockOrderDto; 14 | import com.example.exception.CustomException; 15 | import com.example.seckill.ISeckillService; 16 | import com.example.util.JedisUtil; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.stereotype.Service; 21 | import redis.clients.jedis.Jedis; 22 | import redis.clients.jedis.Transaction; 23 | 24 | import java.util.List; 25 | 26 | /** 27 | * 使用乐观锁加缓存的方式(线程不安全) 28 | * 29 | * @author wliduo[i@dolyw.com] 30 | * @date 2019-11-20 18:03:33 31 | */ 32 | @Service("seckillOptimisticLockRedisWrongService") 33 | public class SeckillOptimisticLockRedisWrongServiceImpl implements ISeckillService { 34 | 35 | /** 36 | * logger 37 | */ 38 | private static final Logger logger = LoggerFactory.getLogger(SeckillOptimisticLockRedisWrongServiceImpl.class); 39 | 40 | @Autowired 41 | private StockDao stockDao; 42 | 43 | @Autowired 44 | private StockOrderDao stockOrderDao; 45 | 46 | @Override 47 | public StockDto checkStock(Integer id) throws Exception { 48 | // 使用缓存读取库存,减轻DB压力,这里会出现数据不一致 49 | Integer count = Integer.parseInt(JedisUtil.get(Constant.PREFIX_COUNT + id)); 50 | Thread.sleep(100); 51 | Integer sale = Integer.parseInt(JedisUtil.get(Constant.PREFIX_SALE + id)); 52 | Thread.sleep(100); 53 | // 第一个线程和第二个线程同时读取缓存count时,都读取到10,然后第二个线程暂停了,第一个线程继续执行, 54 | // 读取的version版本号为0,继续执行到已经秒杀完成,更新缓存(version版本号加一,变成1), 55 | // 现在第二个线程才恢复继续执行,结果读取缓存version版本号为1(本来应该也是0) 56 | Integer version = Integer.parseInt(JedisUtil.get(Constant.PREFIX_VERSION + id)); 57 | if (count > 0) { 58 | // 还有库存 59 | StockDto stockDto = new StockDto(); 60 | stockDto.setId(id); 61 | stockDto.setCount(count); 62 | stockDto.setSale(sale); 63 | stockDto.setVersion(version); 64 | return stockDto; 65 | } 66 | throw new CustomException("库存不足"); 67 | } 68 | 69 | @Override 70 | public Integer saleStock(StockDto stockDto) throws Exception { 71 | Integer saleCount = stockDao.updateByOptimisticLock(stockDto); 72 | // 操作数据大于0,说明扣库存成功 73 | if (saleCount > 0) { 74 | logger.info("版本号:{} {} {}", stockDto.getCount(), stockDto.getSale(), stockDto.getVersion()); 75 | Thread.sleep(10); 76 | // 更新缓存,这里更新需要保证三个数据(库存,已售,乐观锁版本号)的一致性,使用Redis事务 77 | updateCache(stockDto); 78 | } 79 | return saleCount; 80 | } 81 | 82 | /** 83 | * 这里遵循先更新数据库,再更新缓存,详细的数据库与缓存一致性解析可以查看 84 | * https://note.dolyw.com/cache/00-DataBaseConsistency.html 85 | * 86 | * @param stockDto 87 | * @return java.lang.Integer 88 | * @throws 89 | * @author wliduo[i@dolyw.com] 90 | * @date 2019/11/22 16:25 91 | */ 92 | public void updateCache(StockDto stockDto) { 93 | // 获取事务 94 | Transaction transaction = null; 95 | try (Jedis jedis = JedisUtil.getJedis()) { 96 | // watch监视key,当事务执行之前这个key发生了改变,事务会被打断 97 | jedis.watch(Constant.PREFIX_COUNT + stockDto.getId(), 98 | Constant.PREFIX_SALE + stockDto.getId(), 99 | Constant.PREFIX_VERSION + stockDto.getId()); 100 | // 开始事务 101 | transaction = jedis.multi(); 102 | // 原子操作库存减一 103 | transaction.decr(Constant.PREFIX_COUNT + stockDto.getId()); 104 | // 原子操作已售数和乐观锁版本号加一 105 | transaction.incr(Constant.PREFIX_SALE + stockDto.getId()); 106 | transaction.incr(Constant.PREFIX_VERSION + stockDto.getId()); 107 | // 执行exec后就会自动执行jedis.unwatch()操作 108 | List result = transaction.exec(); 109 | if (result == null || result.isEmpty()) { 110 | // 事务失败了,可能是watch-key被外部修改,或者是数据操作被驳回 111 | logger.error("更新缓存失败: {}", stockDto.toString()); 112 | // watch-key被外部修改时,transaction.discard()操作会被自动触发 113 | } 114 | } catch (Exception e) { 115 | logger.error("更新缓存失败: {},异常原因: {}", stockDto.toString(), e.getMessage()); 116 | if (transaction != null) { 117 | // 关闭事务 118 | transaction.discard(); 119 | } 120 | } 121 | } 122 | 123 | @Override 124 | public Integer createOrder(StockDto stockDto) { 125 | StockOrderDto stockOrderDto = new StockOrderDto(); 126 | stockOrderDto.setStockId(stockDto.getId()); 127 | return stockOrderDao.insertSelective(stockOrderDto); 128 | } 129 | 130 | } -------------------------------------------------------------------------------- /src/main/java/com/example/seckill/impl/SeckillOptimisticLockServiceImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.seckill.impl; 7 | 8 | 9 | import com.example.dao.StockDao; 10 | import com.example.dao.StockOrderDao; 11 | import com.example.dto.custom.StockDto; 12 | import com.example.dto.custom.StockOrderDto; 13 | import com.example.exception.CustomException; 14 | import com.example.seckill.ISeckillService; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.stereotype.Service; 17 | 18 | /** 19 | * 使用乐观锁 20 | * 21 | * @author wliduo[i@dolyw.com] 22 | * @date 2019-11-20 18:03:33 23 | */ 24 | @Service("seckillOptimisticLockService") 25 | public class SeckillOptimisticLockServiceImpl implements ISeckillService { 26 | 27 | @Autowired 28 | private StockDao stockDao; 29 | 30 | @Autowired 31 | private StockOrderDao stockOrderDao; 32 | 33 | @Override 34 | public StockDto checkStock(Integer id) { 35 | StockDto stockDto = stockDao.selectByPrimaryKey(id); 36 | if (stockDto.getCount() > 0) { 37 | return stockDto; 38 | } 39 | throw new CustomException("库存不足"); 40 | } 41 | 42 | @Override 43 | public Integer saleStock(StockDto stockDto) { 44 | return stockDao.updateByOptimisticLock(stockDto); 45 | } 46 | 47 | @Override 48 | public Integer createOrder(StockDto stockDto) { 49 | StockOrderDto stockOrderDto = new StockOrderDto(); 50 | stockOrderDto.setStockId(stockDto.getId()); 51 | return stockOrderDao.insertSelective(stockOrderDto); 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/com/example/seckill/impl/SeckillTraditionServiceImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.seckill.impl; 7 | 8 | 9 | import com.example.dao.StockDao; 10 | import com.example.dao.StockOrderDao; 11 | import com.example.dto.custom.StockDto; 12 | import com.example.dto.custom.StockOrderDto; 13 | import com.example.exception.CustomException; 14 | import com.example.seckill.ISeckillService; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.stereotype.Service; 17 | 18 | /** 19 | * 传统方式(并发会出现错误) 20 | * 21 | * @author wliduo[i@dolyw.com] 22 | * @date 2019-11-20 18:03:33 23 | */ 24 | @Service("seckillTraditionService") 25 | public class SeckillTraditionServiceImpl implements ISeckillService { 26 | 27 | @Autowired 28 | private StockDao stockDao; 29 | 30 | @Autowired 31 | private StockOrderDao stockOrderDao; 32 | 33 | @Override 34 | public StockDto checkStock(Integer id) { 35 | StockDto stockDto = stockDao.selectByPrimaryKey(id); 36 | if (stockDto.getCount() > 0) { 37 | return stockDto; 38 | } 39 | throw new CustomException("库存不足"); 40 | } 41 | 42 | @Override 43 | public Integer saleStock(StockDto stockDto) { 44 | stockDto.setCount(stockDto.getCount() - 1); 45 | stockDto.setSale(stockDto.getSale() + 1); 46 | return stockDao.updateByPrimaryKey(stockDto); 47 | } 48 | 49 | @Override 50 | public Integer createOrder(StockDto stockDto) { 51 | StockOrderDto stockOrderDto = new StockOrderDto(); 52 | stockOrderDto.setStockId(stockDto.getId()); 53 | return stockOrderDao.insertSelective(stockOrderDto); 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/java/com/example/service/ISeckillEvolutionService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.service; 7 | 8 | 9 | /** 10 | * ISeckillEvolutionService 11 | * 12 | * @author wliduo[i@dolyw.com] 13 | * @date 2019-11-20 18:03:33 14 | */ 15 | public interface ISeckillEvolutionService { 16 | 17 | /** 18 | * 传统方式的创建订单(并发会出现错误) 19 | * 20 | * @param id 21 | * @return java.lang.Integer 22 | * @throws Exception 23 | * @author wliduo[i@dolyw.com] 24 | * @date 2019/11/22 14:21 25 | */ 26 | Integer createWrongOrder(Integer id) throws Exception; 27 | 28 | /** 29 | * 使用乐观锁创建订单(解决卖超问题) 30 | * 31 | * @param id 32 | * @return java.lang.Integer 33 | * @throws Exception 34 | * @author wliduo[i@dolyw.com] 35 | * @date 2019/11/22 14:21 36 | */ 37 | Integer createOptimisticLockOrder(Integer id) throws Exception; 38 | 39 | /** 40 | * 使用乐观锁创建订单(解决卖超问题),加缓存读(线程不安全),提升性能, 41 | * 42 | * @param id 43 | * @return java.lang.Integer 44 | * @throws Exception 45 | * @author wliduo[i@dolyw.com] 46 | * @date 2019/11/22 14:21 47 | */ 48 | Integer createOptimisticLockOrderWithRedisWrong(Integer id) throws Exception; 49 | 50 | /** 51 | * 使用乐观锁创建订单(解决卖超问题),加缓存读(线程安全),提升性能, 52 | * 53 | * @param id 54 | * @return java.lang.Integer 55 | * @throws Exception 56 | * @author wliduo[i@dolyw.com] 57 | * @date 2019/11/22 14:21 58 | */ 59 | Integer createOptimisticLockOrderWithRedisSafe(Integer id) throws Exception; 60 | 61 | } -------------------------------------------------------------------------------- /src/main/java/com/example/service/IStockOrderService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.service; 7 | 8 | import com.example.common.IBaseService; 9 | import com.example.dto.custom.StockOrderDto; 10 | import java.util.List; 11 | 12 | /** 13 | * IStockOrderService 14 | * @author wliduo[i@dolyw.com] 15 | * @date 2019-11-20 18:03:33 16 | */ 17 | public interface IStockOrderService extends IBaseService { 18 | 19 | /** 20 | * 列表 21 | * @param stockOrderDto 22 | * @return java.util.List 23 | * @author wliduo[i@dolyw.com] 24 | * @date 2019-11-20 18:03:33 25 | */ 26 | public List findPageInfo(StockOrderDto stockOrderDto); 27 | } -------------------------------------------------------------------------------- /src/main/java/com/example/service/IStockService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.service; 7 | 8 | import com.example.common.IBaseService; 9 | import com.example.dto.custom.StockDto; 10 | import java.util.List; 11 | 12 | /** 13 | * IStockService 14 | * @author wliduo[i@dolyw.com] 15 | * @date 2019-11-20 18:03:33 16 | */ 17 | public interface IStockService extends IBaseService { 18 | 19 | /** 20 | * 列表 21 | * @param stockDto 22 | * @return java.util.List 23 | * @author wliduo[i@dolyw.com] 24 | * @date 2019-11-20 18:03:33 25 | */ 26 | public List findPageInfo(StockDto stockDto); 27 | } -------------------------------------------------------------------------------- /src/main/java/com/example/service/impl/SeckillEvolutionServiceImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.service.impl; 7 | 8 | 9 | import com.example.dto.custom.StockDto; 10 | import com.example.exception.CustomException; 11 | import com.example.seckill.ISeckillService; 12 | import com.example.service.ISeckillEvolutionService; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.beans.factory.annotation.Qualifier; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.annotation.Transactional; 17 | 18 | /** 19 | * StockServiceImpl 20 | * 21 | * @author wliduo[i@dolyw.com] 22 | * @date 2019-11-20 18:03:33 23 | */ 24 | @Service("seckillEvolutionService") 25 | public class SeckillEvolutionServiceImpl implements ISeckillEvolutionService { 26 | 27 | /** 28 | * 传统方式(名称注入) 29 | */ 30 | @Autowired 31 | @Qualifier("seckillTraditionService") 32 | private ISeckillService seckillTraditionService; 33 | 34 | /** 35 | * 乐观锁方式(名称注入) 36 | */ 37 | @Autowired 38 | @Qualifier("seckillOptimisticLockService") 39 | private ISeckillService seckillOptimisticLockServiceImpl; 40 | 41 | /** 42 | * 乐观锁加缓存方法(名称注入),线程不安全 43 | */ 44 | @Autowired 45 | @Qualifier("seckillOptimisticLockRedisWrongService") 46 | private ISeckillService SeckillOptimisticLockRedisWrongServiceImpl; 47 | 48 | /** 49 | * 乐观锁加缓存方法(名称注入),线程安全 50 | */ 51 | @Autowired 52 | @Qualifier("seckillOptimisticLockRedisSafeService") 53 | private ISeckillService seckillOptimisticLockRedisSafeService; 54 | 55 | @Override 56 | @Transactional(rollbackFor = Exception.class) 57 | public Integer createWrongOrder(Integer id) throws Exception { 58 | // 检查库存 59 | StockDto stockDto = seckillTraditionService.checkStock(id); 60 | // 扣库存 61 | Integer saleCount = seckillTraditionService.saleStock(stockDto); 62 | if (saleCount <= 0) { 63 | throw new CustomException("扣库存失败"); 64 | } 65 | // 下订单 66 | Integer orderCount = seckillTraditionService.createOrder(stockDto); 67 | if (saleCount <= 0) { 68 | throw new CustomException("下订单失败"); 69 | } 70 | return orderCount; 71 | } 72 | 73 | @Override 74 | @Transactional(rollbackFor = Exception.class) 75 | public Integer createOptimisticLockOrder(Integer id) throws Exception { 76 | // 检查库存 77 | StockDto stockDto = seckillOptimisticLockServiceImpl.checkStock(id); 78 | Thread.sleep(10); 79 | // 扣库存 80 | Integer saleCount = seckillOptimisticLockServiceImpl.saleStock(stockDto); 81 | if (saleCount <= 0) { 82 | throw new CustomException("扣库存失败"); 83 | } 84 | // 下订单 85 | Integer orderCount = seckillOptimisticLockServiceImpl.createOrder(stockDto); 86 | if (saleCount <= 0) { 87 | throw new CustomException("下订单失败"); 88 | } 89 | return orderCount; 90 | } 91 | 92 | @Override 93 | @Transactional(rollbackFor = Exception.class) 94 | public Integer createOptimisticLockOrderWithRedisWrong(Integer id) throws Exception { 95 | // 检查库存 96 | StockDto stockDto = SeckillOptimisticLockRedisWrongServiceImpl.checkStock(id); 97 | // 扣库存 98 | Integer saleCount = SeckillOptimisticLockRedisWrongServiceImpl.saleStock(stockDto); 99 | if (saleCount <= 0) { 100 | throw new CustomException("扣库存失败"); 101 | } 102 | Thread.sleep(10); 103 | // 下订单 104 | Integer orderCount = SeckillOptimisticLockRedisWrongServiceImpl.createOrder(stockDto); 105 | if (saleCount <= 0) { 106 | throw new CustomException("下订单失败"); 107 | } 108 | return orderCount; 109 | } 110 | 111 | @Override 112 | @Transactional(rollbackFor = Exception.class) 113 | public Integer createOptimisticLockOrderWithRedisSafe(Integer id) throws Exception { 114 | // 检查库存 115 | StockDto stockDto = seckillOptimisticLockRedisSafeService.checkStock(id); 116 | // 扣库存 117 | Integer saleCount = seckillOptimisticLockRedisSafeService.saleStock(stockDto); 118 | if (saleCount <= 0) { 119 | throw new CustomException("扣库存失败"); 120 | } 121 | Thread.sleep(10); 122 | // 下订单 123 | Integer orderCount = seckillOptimisticLockRedisSafeService.createOrder(stockDto); 124 | if (saleCount <= 0) { 125 | throw new CustomException("下订单失败"); 126 | } 127 | Thread.sleep(10); 128 | return orderCount; 129 | } 130 | } -------------------------------------------------------------------------------- /src/main/java/com/example/service/impl/StockOrderServiceImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.service.impl; 7 | 8 | import com.example.common.impl.BaseServiceImpl; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | 12 | import com.example.dao.StockOrderDao; 13 | import com.example.dto.custom.StockOrderDto; 14 | import com.example.service.IStockOrderService; 15 | import java.util.List; 16 | 17 | /** 18 | * StockOrderServiceImpl 19 | * @author wliduo[i@dolyw.com] 20 | * @date 2019-11-20 18:03:33 21 | */ 22 | @Service("stockOrderService") 23 | public class StockOrderServiceImpl extends BaseServiceImpl implements IStockOrderService { 24 | 25 | @Autowired 26 | private StockOrderDao stockOrderDao; 27 | 28 | @Override 29 | public List findPageInfo(StockOrderDto stockOrderDto) { 30 | return stockOrderDao.findPageInfo(stockOrderDto); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/com/example/service/impl/StockServiceImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * PDMS wliduo https://github.com/dolyw 3 | * Created By dolyw.com 4 | * Date By (2019-11-20 18:03:33) 5 | */ 6 | package com.example.service.impl; 7 | 8 | 9 | import com.example.common.impl.BaseServiceImpl; 10 | import com.example.dao.StockDao; 11 | import com.example.dto.custom.StockDto; 12 | import com.example.service.IStockService; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.List; 17 | 18 | /** 19 | * StockServiceImpl 20 | * @author wliduo[i@dolyw.com] 21 | * @date 2019-11-20 18:03:33 22 | */ 23 | @Service("stockService") 24 | public class StockServiceImpl extends BaseServiceImpl implements IStockService { 25 | 26 | @Autowired 27 | private StockDao stockDao; 28 | 29 | @Override 30 | public List findPageInfo(StockDto stockDto) { 31 | return stockDao.findPageInfo(stockDto); 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/com/example/util/JedisUtil.java: -------------------------------------------------------------------------------- 1 | package com.example.util; 2 | 3 | import com.example.constant.Constant; 4 | import com.example.exception.CustomException; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | import redis.clients.jedis.Jedis; 8 | import redis.clients.jedis.JedisPool; 9 | 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | /** 14 | * JedisUtil 15 | * 16 | * @author wliduo[i@dolyw.com] 17 | * @date 2019/11/14 16:44 18 | */ 19 | @Component 20 | public class JedisUtil { 21 | 22 | /** 23 | * 静态注入JedisPool连接池 24 | * 现在改为静态注入JedisPool连接池,JedisUtil直接调用静态方法即可 25 | * https://blog.csdn.net/W_Z_W_888/article/details/79979103 26 | */ 27 | private static JedisPool jedisPool; 28 | 29 | @Autowired 30 | public void setJedisPool(JedisPool jedisPool) { 31 | JedisUtil.jedisPool = jedisPool; 32 | } 33 | 34 | /** 35 | * 获取Jedis实例 36 | * 37 | * @param 38 | * @return redis.clients.jedis.Jedis 39 | * @throws 40 | * @author wliduo[i@dolyw.com] 41 | * @date 2019/11/14 16:45 42 | */ 43 | public static Jedis getJedis() { 44 | try { 45 | if (jedisPool != null) { 46 | return jedisPool.getResource(); 47 | } else { 48 | throw new CustomException("获取Jedis资源异常"); 49 | } 50 | } catch (Exception e) { 51 | throw new CustomException("获取Jedis资源异常:" + e.getMessage()); 52 | } 53 | } 54 | 55 | /** 56 | * 获取redis键值 57 | * 58 | * @param key 59 | * @return java.lang.String 60 | * @throws 61 | * @author wliduo[i@dolyw.com] 62 | * @date 2019/11/14 16:45 63 | */ 64 | public static String get(String key) { 65 | try (Jedis jedis = jedisPool.getResource()) { 66 | return jedis.get(key); 67 | } catch (Exception e) { 68 | throw new CustomException("获取Redis键值get方法异常:key=" + key + " cause=" + e.getMessage()); 69 | } 70 | } 71 | 72 | /** 73 | * 批量获取redis键值 74 | * 75 | * @param keys 76 | * @return java.lang.String 77 | * @throws 78 | * @author wliduo[i@dolyw.com] 79 | * @date 2019/11/14 16:45 80 | */ 81 | public static List mget(String... keys) { 82 | try (Jedis jedis = jedisPool.getResource()) { 83 | return jedis.mget(keys); 84 | } catch (Exception e) { 85 | throw new CustomException("获取Redis键值mget方法异常:key=" + keys + " cause=" + e.getMessage()); 86 | } 87 | } 88 | 89 | /** 90 | * 设置redis键值 91 | * 92 | * @param key 93 | * @param value 94 | * @return java.lang.String 95 | * @throws 96 | * @author wliduo[i@dolyw.com] 97 | * @date 2019/11/14 16:45 98 | */ 99 | public static String set(String key, String value) { 100 | try (Jedis jedis = jedisPool.getResource()) { 101 | return jedis.set(key, value); 102 | } catch (Exception e) { 103 | throw new CustomException("设置Redis键值set方法异常:key=" + key + " value=" + value + " cause=" + e.getMessage()); 104 | } 105 | } 106 | 107 | /** 108 | * 批量设置redis键值 109 | * 110 | * @param keysvalues 111 | * @return java.lang.String 112 | * @throws 113 | * @author wliduo[i@dolyw.com] 114 | * @date 2019/11/14 16:45 115 | */ 116 | public static String mset(String... keysvalues) { 117 | try (Jedis jedis = jedisPool.getResource()) { 118 | return jedis.mset(keysvalues); 119 | } catch (Exception e) { 120 | throw new CustomException("设置Redis键值set方法异常:keysvalues=" + keysvalues + " cause=" + e.getMessage()); 121 | } 122 | } 123 | 124 | /** 125 | * 设置redis键值 126 | * 127 | * @param key 128 | * @param value 129 | * @param expire 130 | * @return java.lang.String 131 | * @throws 132 | * @author wliduo[i@dolyw.com] 133 | * @date 2019/11/14 16:45 134 | */ 135 | public static String set(String key, String value, int expire) { 136 | String result; 137 | try (Jedis jedis = jedisPool.getResource()) { 138 | result = jedis.set(key, value); 139 | if (Constant.OK.equals(result)) { 140 | jedis.expire(key, expire); 141 | } 142 | return result; 143 | } catch (Exception e) { 144 | throw new CustomException("设置Redis键值set方法异常:key=" + key + " value=" + value + " cause=" + e.getMessage()); 145 | } 146 | } 147 | 148 | /** 149 | * 删除key 150 | * 151 | * @param key 152 | * @return java.lang.Long 153 | * @author wliduo[i@dolyw.com] 154 | * @date 2018/9/4 15:50 155 | */ 156 | public static Long del(String key) { 157 | try (Jedis jedis = jedisPool.getResource()) { 158 | return jedis.del(key.getBytes()); 159 | } catch (Exception e) { 160 | throw new CustomException("删除Redis的键delKey方法异常:key=" + key + " cause=" + e.getMessage()); 161 | } 162 | } 163 | 164 | /** 165 | * 键值自增 166 | * 167 | * @param key 168 | * @return java.lang.Long 169 | * @throws 170 | * @author wliduo[i@dolyw.com] 171 | * @date 2019/11/14 17:18 172 | */ 173 | public static Long incr(String key) { 174 | try (Jedis jedis = jedisPool.getResource()) { 175 | return jedis.incr(key); 176 | } catch (Exception e) { 177 | throw new CustomException("键值自增incr方法异常:key=" + key + " cause=" + e.getMessage()); 178 | } 179 | } 180 | 181 | /** 182 | * 键值自减 183 | * 184 | * @param key 185 | * @return java.lang.Long 186 | * @throws 187 | * @author wliduo[i@dolyw.com] 188 | * @date 2019/11/14 17:18 189 | */ 190 | public static Long decr(String key) { 191 | try (Jedis jedis = jedisPool.getResource()) { 192 | return jedis.decr(key); 193 | } catch (Exception e) { 194 | throw new CustomException("键值自减decr方法异常:key=" + key + " cause=" + e.getMessage()); 195 | } 196 | } 197 | 198 | /** 199 | * key是否存在 200 | * 201 | * @param key 202 | * @return java.lang.Boolean 203 | * @author wliduo[i@dolyw.com] 204 | * @date 2018/9/4 15:51 205 | */ 206 | public static Boolean exists(String key) { 207 | try (Jedis jedis = jedisPool.getResource()) { 208 | return jedis.exists(key.getBytes()); 209 | } catch (Exception e) { 210 | throw new CustomException("查询Redis的键是否存在exists方法异常:key=" + key + " cause=" + e.getMessage()); 211 | } 212 | } 213 | 214 | /** 215 | * 模糊查询获取key集合(keys的速度非常快,但在一个大的数据库中使用它仍然可能造成性能问题,生产不推荐使用) 216 | * 217 | * @param key 218 | * @return java.util.Set 219 | * @author wliduo[i@dolyw.com] 220 | * @date 2018/9/6 9:43 221 | */ 222 | public static Set keysS(String key) { 223 | try (Jedis jedis = jedisPool.getResource()) { 224 | return jedis.keys(key); 225 | } catch (Exception e) { 226 | throw new CustomException("模糊查询Redis的键集合keysS方法异常:key=" + key + " cause=" + e.getMessage()); 227 | } 228 | } 229 | 230 | /** 231 | * 模糊查询获取key集合(keys的速度非常快,但在一个大的数据库中使用它仍然可能造成性能问题,生产不推荐使用) 232 | * 233 | * @param key 234 | * @return java.util.Set 235 | * @author wliduo[i@dolyw.com] 236 | * @date 2018/9/6 9:43 237 | */ 238 | public static Set keysB(String key) { 239 | try (Jedis jedis = jedisPool.getResource()) { 240 | return jedis.keys(key.getBytes()); 241 | } catch (Exception e) { 242 | throw new CustomException("模糊查询Redis的键集合keysB方法异常:key=" + key + " cause=" + e.getMessage()); 243 | } 244 | } 245 | 246 | /** 247 | * 获取过期剩余时间 248 | * 249 | * @param key 250 | * @return java.lang.String 251 | * @author wliduo[i@dolyw.com] 252 | * @date 2018/9/11 16:26 253 | */ 254 | public static Long ttl(String key) { 255 | Long result = -2L; 256 | try (Jedis jedis = jedisPool.getResource()) { 257 | result = jedis.ttl(key); 258 | return result; 259 | } catch (Exception e) { 260 | throw new CustomException("获取Redis键过期剩余时间ttl方法异常:key=" + key + " cause=" + e.getMessage()); 261 | } 262 | } 263 | 264 | /** 265 | * 脚本执行 266 | * 267 | * @param script 268 | * @param keys 269 | * @param args 270 | * @return java.lang.Long 271 | * @throws 272 | * @author wliduo[i@dolyw.com] 273 | * @date 2019/11/25 16:50 274 | */ 275 | public static Object eval(String script, List keys, List args) { 276 | Object result = null; 277 | try (Jedis jedis = jedisPool.getResource()) { 278 | result = jedis.eval(script, keys, args); 279 | return result; 280 | } catch (Exception e) { 281 | throw new CustomException("Redis脚本执行eval方法异常:script=" + script + " keys=" + 282 | keys.toString() + " args=" + args.toString() + " cause=" + e.getMessage()); 283 | } 284 | } 285 | 286 | } 287 | -------------------------------------------------------------------------------- /src/main/java/com/example/util/RedisLimitUtil.java: -------------------------------------------------------------------------------- 1 | package com.example.util; 2 | 3 | import com.example.exception.CustomException; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | /** 18 | * RedisLimitUtil 19 | * 20 | * @author wliduo[i@dolyw.com] 21 | * @date 2019/11/14 16:44 22 | */ 23 | @Component 24 | public class RedisLimitUtil { 25 | 26 | /** 27 | * logger 28 | */ 29 | private static final Logger logger = LoggerFactory.getLogger(RedisLimitUtil.class); 30 | 31 | /** 32 | * 秒级限流(每秒限制多少请求)字符串脚本 33 | */ 34 | private static String LIMIT_SECKILL_SCRIPT = null; 35 | 36 | /** 37 | * 自定义参数限流(自定义多少时间限制多少请求)字符串脚本 38 | */ 39 | private static String LIMIT_CUSTOM_SCRIPT = null; 40 | 41 | /** 42 | * redis-key-前缀-limit-限流 43 | */ 44 | private static final String LIMIT = "limit:"; 45 | 46 | /** 47 | * redis-key-名称-limit-一个时间窗口内请求的数量累计(限流累计请求数) 48 | */ 49 | private static final String LIMIT_REQUEST = "limit:request"; 50 | 51 | /** 52 | * redis-key-名称-limit-一个时间窗口开始时间(限流开始时间) 53 | */ 54 | private static final String LIMIT_TIME = "limit:time"; 55 | 56 | /** 57 | * 构造方法初始化加载Lua脚本 58 | */ 59 | public RedisLimitUtil() { 60 | LIMIT_SECKILL_SCRIPT = getScript("redis/limit-seckill.lua"); 61 | LIMIT_CUSTOM_SCRIPT = getScript("redis/limit-custom.lua"); 62 | } 63 | 64 | /** 65 | * 秒级限流判断(每秒限制多少请求) 66 | * 67 | * @param maxRequest 限流最大请求数 68 | * @return boolean 69 | * @throws 70 | * @author wliduo[i@dolyw.com] 71 | * @date 2019/11/25 17:57 72 | */ 73 | public Long limit(String maxRequest) { 74 | // 获取key名,当前时间戳 75 | String key = LIMIT + String.valueOf(System.currentTimeMillis() / 1000); 76 | // 传入参数,限流最大请求数 77 | List args = new ArrayList<>(); 78 | args.add(maxRequest); 79 | return eval(LIMIT_SECKILL_SCRIPT, Collections.singletonList(key), args); 80 | } 81 | 82 | /** 83 | * 自定义参数限流判断(自定义多少时间限制多少请求) 84 | * 85 | * @param maxRequest 限流最大请求数 86 | * @param timeRequest 一个时间窗口(秒) 87 | * @return boolean 88 | * @throws 89 | * @author wliduo[i@dolyw.com] 90 | * @date 2019/11/25 17:57 91 | */ 92 | public Long limit(String maxRequest, String timeRequest) { 93 | // 获取key名,一个时间窗口开始时间(限流开始时间)和一个时间窗口内请求的数量累计(限流累计请求数) 94 | List keys = new ArrayList<>(); 95 | keys.add(LIMIT_TIME); 96 | keys.add(LIMIT_REQUEST); 97 | // 传入参数,限流最大请求数,当前时间戳,一个时间窗口时间(毫秒)(限流时间) 98 | List args = new ArrayList<>(); 99 | args.add(maxRequest); 100 | args.add(String.valueOf(System.currentTimeMillis())); 101 | args.add(timeRequest); 102 | return eval(LIMIT_CUSTOM_SCRIPT, keys, args); 103 | } 104 | 105 | /** 106 | * 执行Lua脚本方法 107 | * 108 | * @param script 109 | * @param keys 110 | * @param args 111 | * @return java.lang.Object 112 | * @throws 113 | * @author wliduo[i@dolyw.com] 114 | * @date 2019/11/26 10:50 115 | */ 116 | private Long eval(String script, List keys, List args) { 117 | // 执行脚本 118 | Object result = JedisUtil.eval(script, keys, args); 119 | // 结果请求数大于0说明不被限流 120 | return (Long) result; 121 | } 122 | 123 | /** 124 | * 获取Lua脚本 125 | * 126 | * @param path 127 | * @return java.lang.String 128 | * @throws 129 | * @author wliduo[i@dolyw.com] 130 | * @date 2019/11/25 17:57 131 | */ 132 | private static String getScript(String path) { 133 | StringBuilder stringBuilder = new StringBuilder(); 134 | InputStream inputStream = RedisLimitUtil.class.getClassLoader().getResourceAsStream(path); 135 | try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { 136 | String str; 137 | while ((str = bufferedReader.readLine()) != null) { 138 | stringBuilder.append(str).append(System.lineSeparator()); 139 | } 140 | } catch (IOException e) { 141 | logger.error(Arrays.toString(e.getStackTrace())); 142 | throw new CustomException("获取Lua限流脚本出现问题: " + Arrays.toString(e.getStackTrace())); 143 | } 144 | return stringBuilder.toString(); 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | datasource: 6 | name: SeckillEvolution 7 | url: jdbc:mysql://127.0.0.1:3306/seckill?serverTimezone=GMT%2B8&useSSL=false&useUnicode=true&characterEncoding=UTF-8 8 | username: root 9 | password: root 10 | # 使用Druid数据源 11 | type: com.alibaba.druid.pool.DruidDataSource 12 | driver-class-name: com.mysql.cj.jdbc.Driver 13 | druid: 14 | filters: stat 15 | maxActive: 20 16 | initialSize: 1 17 | maxWait: 60000 18 | minIdle: 1 19 | timeBetweenEvictionRunsMillis: 60000 20 | minEvictableIdleTimeMillis: 300000 21 | validationQuery: select 'x' 22 | testWhileIdle: true 23 | testOnBorrow: false 24 | testOnReturn: false 25 | poolPreparedStatements: true 26 | maxOpenPreparedStatements: 20 27 | # 404交给异常处理器处理 28 | mvc: 29 | throw-exception-if-no-handler-found: true 30 | # 404交给异常处理器处理 31 | resources: 32 | add-mappings: false 33 | 34 | mybatis: 35 | # Mybatis配置Mapper路径 36 | mapper-locations: classpath:mapper/*.xml 37 | # Mybatis配置Model类对应 38 | type-aliases-package: com.example.dto.custom 39 | 40 | pagehelper: 41 | params: count=countSql 42 | # 指定分页插件使用哪种方言 43 | helper-dialect: mysql 44 | # 分页合理化参数 pageNum<=0时会查询第一页 pageNum>pages(超过总数时) 会查询最后一页 45 | reasonable: 'true' 46 | support-methods-arguments: 'true' 47 | 48 | mapper: 49 | # 通用Mapper的insertSelective和updateByPrimaryKeySelective中是否判断字符串类型!='' 50 | not-empty: true 51 | 52 | logging: 53 | # Debug打印SQL 54 | level.com.example.dao: debug 55 | 56 | redis: 57 | # Redis服务器地址 58 | host: 127.0.0.1 59 | # Redis服务器连接端口 60 | port: 6379 61 | # Redis服务器连接密码(默认为空) 62 | password: 63 | # 连接超时时间(毫秒) 64 | timeout: 10000 65 | pool: 66 | # 连接池最大连接数(使用负值表示没有限制) 67 | max-active: 200 68 | # 连接池最大阻塞等待时间(使用负值表示没有限制) 69 | max-wait: -1 70 | # 连接池中的最大空闲连接 71 | max-idle: 8 72 | # 连接池中的最小空闲连接 73 | min-idle: 0 -------------------------------------------------------------------------------- /src/main/resources/jmx/乐观锁加缓存加限流秒杀测试.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 | 500 22 | 5 23 | false 24 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | localhost 34 | 8080 35 | http 36 | 37 | 38 | 6 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | seckill/createOptimisticLockOrderWithRedisLimit/1 52 | POST 53 | true 54 | false 55 | true 56 | false 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 200 69 | 70 | test error 71 | Assertion.response_code 72 | false 73 | 8 74 | 75 | 76 | 77 | false 78 | 79 | saveConfig 80 | 81 | 82 | true 83 | true 84 | true 85 | 86 | true 87 | true 88 | true 89 | true 90 | false 91 | true 92 | true 93 | false 94 | false 95 | false 96 | true 97 | false 98 | false 99 | false 100 | true 101 | 0 102 | true 103 | true 104 | true 105 | true 106 | true 107 | true 108 | 109 | 110 | 111 | 112 | 113 | 114 | false 115 | 116 | saveConfig 117 | 118 | 119 | true 120 | true 121 | true 122 | 123 | true 124 | true 125 | true 126 | true 127 | false 128 | true 129 | true 130 | false 131 | false 132 | false 133 | true 134 | false 135 | false 136 | false 137 | true 138 | 0 139 | true 140 | true 141 | true 142 | true 143 | true 144 | true 145 | 146 | 147 | 148 | 149 | 150 | 151 | false 152 | 153 | saveConfig 154 | 155 | 156 | true 157 | true 158 | true 159 | 160 | true 161 | true 162 | true 163 | true 164 | false 165 | true 166 | true 167 | false 168 | false 169 | false 170 | true 171 | false 172 | false 173 | false 174 | true 175 | 0 176 | true 177 | true 178 | true 179 | true 180 | true 181 | true 182 | 183 | 184 | 185 | 1000 186 | false 187 | 188 | 189 | 190 | 191 | false 192 | false 193 | 194 | 195 | 196 | false 197 | 198 | saveConfig 199 | 200 | 201 | true 202 | true 203 | true 204 | 205 | true 206 | true 207 | true 208 | true 209 | false 210 | true 211 | true 212 | false 213 | false 214 | false 215 | true 216 | false 217 | false 218 | false 219 | true 220 | 0 221 | true 222 | true 223 | true 224 | true 225 | true 226 | true 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/main/resources/jmx/乐观锁加缓存秒杀测试.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 | 500 22 | 0 23 | false 24 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | localhost 34 | 8080 35 | http 36 | 37 | 38 | 6 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | seckill/createOptimisticLockOrderWithRedis/1 52 | POST 53 | true 54 | false 55 | true 56 | false 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 200 69 | 70 | test error 71 | Assertion.response_code 72 | false 73 | 8 74 | 75 | 76 | 77 | false 78 | 79 | saveConfig 80 | 81 | 82 | true 83 | true 84 | true 85 | 86 | true 87 | true 88 | true 89 | true 90 | false 91 | true 92 | true 93 | false 94 | false 95 | false 96 | true 97 | false 98 | false 99 | false 100 | true 101 | 0 102 | true 103 | true 104 | true 105 | true 106 | true 107 | true 108 | 109 | 110 | 111 | 112 | 113 | 114 | false 115 | 116 | saveConfig 117 | 118 | 119 | true 120 | true 121 | true 122 | 123 | true 124 | true 125 | true 126 | true 127 | false 128 | true 129 | true 130 | false 131 | false 132 | false 133 | true 134 | false 135 | false 136 | false 137 | true 138 | 0 139 | true 140 | true 141 | true 142 | true 143 | true 144 | true 145 | 146 | 147 | 148 | 149 | 150 | 151 | false 152 | 153 | saveConfig 154 | 155 | 156 | true 157 | true 158 | true 159 | 160 | true 161 | true 162 | true 163 | true 164 | false 165 | true 166 | true 167 | false 168 | false 169 | false 170 | true 171 | false 172 | false 173 | false 174 | true 175 | 0 176 | true 177 | true 178 | true 179 | true 180 | true 181 | true 182 | 183 | 184 | 185 | 1000 186 | false 187 | 188 | 189 | 190 | 191 | false 192 | false 193 | 194 | 195 | 196 | false 197 | 198 | saveConfig 199 | 200 | 201 | true 202 | true 203 | true 204 | 205 | true 206 | true 207 | true 208 | true 209 | false 210 | true 211 | true 212 | false 213 | false 214 | false 215 | true 216 | false 217 | false 218 | false 219 | true 220 | 0 221 | true 222 | true 223 | true 224 | true 225 | true 226 | true 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/main/resources/jmx/乐观锁秒杀测试.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 | 500 22 | 0 23 | false 24 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | localhost 34 | 8080 35 | http 36 | 37 | 38 | 6 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | seckill/createOptimisticLockOrder/1 52 | POST 53 | true 54 | false 55 | true 56 | false 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 200 69 | 70 | test error 71 | Assertion.response_code 72 | false 73 | 8 74 | 75 | 76 | 77 | false 78 | 79 | saveConfig 80 | 81 | 82 | true 83 | true 84 | true 85 | 86 | true 87 | true 88 | true 89 | true 90 | false 91 | true 92 | true 93 | false 94 | false 95 | false 96 | true 97 | false 98 | false 99 | false 100 | true 101 | 0 102 | true 103 | true 104 | true 105 | true 106 | true 107 | true 108 | 109 | 110 | 111 | 112 | 113 | 114 | false 115 | 116 | saveConfig 117 | 118 | 119 | true 120 | true 121 | true 122 | 123 | true 124 | true 125 | true 126 | true 127 | false 128 | true 129 | true 130 | false 131 | false 132 | false 133 | true 134 | false 135 | false 136 | false 137 | true 138 | 0 139 | true 140 | true 141 | true 142 | true 143 | true 144 | true 145 | 146 | 147 | 148 | 149 | 150 | 151 | false 152 | 153 | saveConfig 154 | 155 | 156 | true 157 | true 158 | true 159 | 160 | true 161 | true 162 | true 163 | true 164 | false 165 | true 166 | true 167 | false 168 | false 169 | false 170 | true 171 | false 172 | false 173 | false 174 | true 175 | 0 176 | true 177 | true 178 | true 179 | true 180 | true 181 | true 182 | 183 | 184 | 185 | 1000 186 | false 187 | 188 | 189 | 190 | 191 | false 192 | false 193 | 194 | 195 | 196 | false 197 | 198 | saveConfig 199 | 200 | 201 | true 202 | true 203 | true 204 | 205 | true 206 | true 207 | true 208 | true 209 | false 210 | true 211 | true 212 | false 213 | false 214 | false 215 | true 216 | false 217 | false 218 | false 219 | true 220 | 0 221 | true 222 | true 223 | true 224 | true 225 | true 226 | true 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/main/resources/jmx/传统方式秒杀测试.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 | 500 22 | 0 23 | false 24 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | localhost 34 | 8080 35 | http 36 | 37 | 38 | 6 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | seckill/createWrongOrder/1 52 | POST 53 | true 54 | false 55 | true 56 | false 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 200 69 | 70 | test error 71 | Assertion.response_code 72 | false 73 | 8 74 | 75 | 76 | 77 | false 78 | 79 | saveConfig 80 | 81 | 82 | true 83 | true 84 | true 85 | 86 | true 87 | true 88 | true 89 | true 90 | false 91 | true 92 | true 93 | false 94 | false 95 | false 96 | true 97 | false 98 | false 99 | false 100 | true 101 | 0 102 | true 103 | true 104 | true 105 | true 106 | true 107 | true 108 | 109 | 110 | 111 | 112 | 113 | 114 | false 115 | 116 | saveConfig 117 | 118 | 119 | true 120 | true 121 | true 122 | 123 | true 124 | true 125 | true 126 | true 127 | false 128 | true 129 | true 130 | false 131 | false 132 | false 133 | true 134 | false 135 | false 136 | false 137 | true 138 | 0 139 | true 140 | true 141 | true 142 | true 143 | true 144 | true 145 | 146 | 147 | 148 | 149 | 150 | 151 | false 152 | 153 | saveConfig 154 | 155 | 156 | true 157 | true 158 | true 159 | 160 | true 161 | true 162 | true 163 | true 164 | false 165 | true 166 | true 167 | false 168 | false 169 | false 170 | true 171 | false 172 | false 173 | false 174 | true 175 | 0 176 | true 177 | true 178 | true 179 | true 180 | true 181 | true 182 | 183 | 184 | 185 | 1000 186 | false 187 | 188 | 189 | 190 | 191 | false 192 | false 193 | 194 | 195 | 196 | false 197 | 198 | saveConfig 199 | 200 | 201 | true 202 | true 203 | true 204 | 205 | true 206 | true 207 | true 208 | true 209 | false 210 | true 211 | true 212 | false 213 | false 214 | false 215 | true 216 | false 217 | false 218 | false 219 | true 220 | 0 221 | true 222 | true 223 | true 224 | true 225 | true 226 | true 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/main/resources/mapper/StockDao.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | g.id AS id, 18 | g.name AS name, 19 | g.count AS count, 20 | g.sale AS sale, 21 | g.version AS version, 22 | g.create_time AS createTime, 23 | g.update_time AS updateTime 24 | 25 | 26 | 62 | 63 | -------------------------------------------------------------------------------- /src/main/resources/mapper/StockOrderDao.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | g.id AS id, 15 | g.stock_id AS stockId, 16 | g.name AS name, 17 | g.create_time AS createTime 18 | 19 | 20 | 44 | 45 | -------------------------------------------------------------------------------- /src/main/resources/redis/limit-custom.lua: -------------------------------------------------------------------------------- 1 | -- 实现原理 2 | -- 每次请求都去 Redis 取到当前限流开始时间和限流累计请求数 3 | -- 判断限流开始时间加超时时间戳(限流时间)大于当前请求时间戳 4 | -- 再判断当前时间窗口请求内是否超过限流最大请求数 5 | -- 当达到阈值时返回错误,表示请求被限流,否则通过 6 | -- 写入 Redis 的操作用 Lua 脚本来完成 7 | -- 利用 Redis 的单线程机制可以保证每个 Redis 请求的原子性 8 | 9 | -- 一个时间窗口开始时间(限流开始时间)key名称 10 | local timeKey = KEYS[1] 11 | -- 一个时间窗口内请求的数量累计(限流累计请求数)key名称 12 | local requestKey = KEYS[2] 13 | -- 限流大小,限流最大请求数 14 | local maxRequest = tonumber(ARGV[1]) 15 | -- 当前请求时间戳 16 | local nowTime = tonumber(ARGV[2]) 17 | -- 超时时间戳,一个时间窗口时间(毫秒)(限流时间) 18 | local timeRequest = tonumber(ARGV[3]) 19 | 20 | -- 获取限流开始时间,不存在为0 21 | local currentTime = tonumber(redis.call('get', timeKey) or "0") 22 | -- 获取限流累计请求数,不存在为0 23 | local currentRequest = tonumber(redis.call('get', requestKey) or "0") 24 | 25 | -- 判断当前请求时间戳是不是在当前时间窗口中 26 | -- 限流开始时间加超时时间戳(限流时间)大于当前请求时间戳 27 | if currentTime + timeRequest > nowTime then 28 | -- 判断当前时间窗口请求内是否超过限流最大请求数 29 | if currentRequest + 1 > maxRequest then 30 | -- 在时间窗口内且超过限流最大请求数,返回 31 | return 0; 32 | else 33 | -- 在时间窗口内且请求数没超,请求数加一 34 | redis.call("INCRBY", requestKey, 1) 35 | return currentRequest + 1; 36 | end 37 | else 38 | -- 超时后重置,开启一个新的时间窗口 39 | redis.call('set', timeKey, nowTime) 40 | redis.call('set', requestKey, '0') 41 | -- 设置过期时间 42 | redis.call("EXPIRE", timeKey, timeRequest / 1000) 43 | redis.call("EXPIRE", requestKey, timeRequest / 1000) 44 | -- 请求数加一 45 | redis.call("INCRBY", requestKey, 1) 46 | return 1; 47 | end -------------------------------------------------------------------------------- /src/main/resources/redis/limit-seckill.lua: -------------------------------------------------------------------------------- 1 | -- 实现原理 2 | -- 每次请求都将当前时间,精确到秒作为 key 放入 Redis 中 3 | -- 超时时间设置为 2s, Redis 将该 key 的值进行自增 4 | -- 当达到阈值时返回错误,表示请求被限流 5 | -- 写入 Redis 的操作用 Lua 脚本来完成 6 | -- 利用 Redis 的单线程机制可以保证每个 Redis 请求的原子性 7 | 8 | -- 资源唯一标志位 9 | local key = KEYS[1] 10 | -- 限流大小 11 | local limit = tonumber(ARGV[1]) 12 | 13 | -- 获取当前流量大小 14 | local currentLimit = tonumber(redis.call('get', key) or "0") 15 | 16 | if currentLimit + 1 > limit then 17 | -- 达到限流大小 返回 18 | return 0; 19 | else 20 | -- 没有达到阈值 value + 1 21 | redis.call("INCRBY", key, 1) 22 | -- 设置过期时间 23 | redis.call("EXPIRE", key, 2) 24 | return currentLimit + 1 25 | end -------------------------------------------------------------------------------- /src/main/resources/sql/MySQL.sql: -------------------------------------------------------------------------------- 1 | --- 删除数据库 2 | drop database seckill; 3 | --- 创建数据库 4 | create database seckill; 5 | --- 使用数据库 6 | use seckill; 7 | --- 创建库存表 8 | DROP TABLE IF EXISTS `t_seckill_stock`; 9 | CREATE TABLE `t_seckill_stock` ( 10 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '库存ID', 11 | `name` varchar(50) NOT NULL DEFAULT 'OnePlus 7 Pro' COMMENT '名称', 12 | `count` int(11) NOT NULL COMMENT '库存', 13 | `sale` int(11) NOT NULL COMMENT '已售', 14 | `version` int(11) NOT NULL COMMENT '乐观锁,版本号', 15 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 16 | `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 17 | PRIMARY KEY (`id`) 18 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='库存表'; 19 | --- 插入一条商品,初始化10个库存 20 | INSERT INTO `t_seckill_stock` (`count`, `sale`, `version`) VALUES ('10', '0', '0'); 21 | --- 创建库存订单表 22 | DROP TABLE IF EXISTS `t_seckill_stock_order`; 23 | CREATE TABLE `t_seckill_stock_order` ( 24 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 25 | `stock_id` int(11) NOT NULL COMMENT '库存ID', 26 | `name` varchar(30) NOT NULL DEFAULT 'OnePlus 7 Pro' COMMENT '商品名称', 27 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', 28 | PRIMARY KEY (`id`) 29 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='库存订单表'; 30 | -------------------------------------------------------------------------------- /src/test/java/com/example/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class ApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | --------------------------------------------------------------------------------