秒杀应用场景
秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
秒杀业务流程比较简单,一般就是下订单减库存。
秒杀的业务场景跟其他业务场景不一样,主要是秒杀的瞬间,并发非常大,如何针对此大并发是我们需要取解决的。秒杀业务,是典型的短时大量突发访问。
秒杀架构设计理念
前端限流:抢购的时候限制每个用户的抢购次数,限制同一个用户多次抢购。
削峰:使用中间件、缓存等技术。
持久化:使用异步处理秒杀是一个高并发系统,可以采用异步处理提高系统效率。
内存缓存:内存的读写效率比写入磁盘块。
秒杀系统架构设计思路
将请求拦截在系统上游,降低下游压力:秒杀系统特点是并发量极大,但实际秒杀成功的请求数量却很少,所以如果不在前端拦截很可能造成数据库读写锁冲突,最终请求超时。
利用缓存:利用缓存可极大提高系统读写速度。
消息队列:消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。
二 安装
redis安装教程
RabbitMQ (otp win64 19.3 rabbitmq-server-3.7.7我的环境)
RabbitMQ安装教程
注意事项:
1.安装时电脑用户名不能为中文,如果你使用的是中文用户,那么请百度如何跳过中文用户安装
2.安装目录不能有中文
3.rabbitmq-plugins enable rabbitmq_management是安装RabbitMQ视图需要连接网络
三
我使用的是maven所有先引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lw</groupId>
<artifactId>lovelw</artifactId>
<version>1.0-SNAPSHOT</version>
<name>demo1</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.5</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.38</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 打jar包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
设置配置信息application.properties
#thymeleaf
spring.thymeleaf.cache=false
spring.thymeleaf.content-type=text/html
spring.thymeleaf.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML5
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
mybatis.type-aliases-package=com.ljs.miaosha.domain
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.default-fetch-size=100
mybatis.configuration.default-statement-timeout=3000
mybatis.mapper-locaitons=classpath:com/ljs/miaosha/dao/*.xml
spring.datasource.url=jdbc:mysql://localhost/miaosha?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.filters=stat
# 初始化大小,最小,最大
spring.datasource.initialSize=100
spring.datasource.minIdle=500
spring.datasource.maxActive=1000
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=30000
spring.datasource.validationQuery=select 'x'
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
#redis 配置服务器等信息
redis.host=127.0.0.1
redis.port=6379
#redis.timeout=10
#如果redis没有设置密码就不需要给密码
#redis.password=123456
redis.poolMaxTotal=1000
redis.poolMaxldle=500
redis.poolMaxWait=500
spring.resources.add-mappings=true
spring.resources.cache-period=3600
spring.resources.chain.cache=true
spring.resources.chain.enabled=true
spring.resources.chain.gzipped=true
spring.resources.chain.html-application-cache=true
spring.resources.static-locations=classpath:/static/
#RabbitMQ配置
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
#消费者数量
spring.rabbitmq.listener.simple.concurrency=10
#消费者最大数量
spring.rabbitmq.listener.simple.max-concurrency=10
#消费,每次从队列中取多少个,取多了,可能处理不过来
spring.rabbitmq.listener.simple.prefetch=1
spring.rabbitmq.listener.auto-startup=true
#消费失败的数据重新压入队列
spring.rabbitmq.listener.simple.default-requeue-rejected=true
#发送,队列满的时候,发送不进去,启动重置
spring.rabbitmq.template.retry.enabled=true
#一秒钟之后重试
spring.rabbitmq.template.retry.initial-interval=1000
#
spring.rabbitmq.template.retry.max-attempts=3
#最大间隔 10s
spring.rabbitmq.template.retry.max-interval=10000
spring.rabbitmq.template.retry.multiplier=1.0
用户登陆验证
@RequestMapping("/login")
@Controller
public class LoginController {
@Autowired
UserService userService;
@Autowired
RedisService redisService;
@Autowired
MiaoshaUserService miaoshaUserService;
//调整登陆
@RequestMapping("/to_login")
public String toLogin() {
return "login";//返回页面login
}
}
MiaoshaController .class
@RequestMapping("/gbf")
@Controller
public class MiaoshaController implements InitializingBean{
@Autowired
GoodsService goodsService;
@Autowired
RedisService redisService;
@Autowired
MiaoshaUserService miaoshaUserService;
//作为秒杀功能事务的Service
@Autowired
MiaoshaService miaoshaService;
@Autowired
OrderService orderService;
@Autowired
MQSender mQSender;
//标记
Map <Long,Boolean>localMap=new HashMap<Long,Boolean>();
/**
* 系统初始化的时候做的事情。
* 在容器启动时候,检测到了实现了接口InitializingBean之后,
*/
public void afterPropertiesSet() throws Exception {
List<GoodsVo> goodslist=goodsService.getGoodsVoList();
if(goodslist==null) {
return;
}
for(GoodsVo goods:goodslist) {
//如果不是null的时候,将库存加载到redis里面去 prefix---GoodsKey:gs , key---商品id, value
redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
}
}
/**
* 生成图片验证码
*/
@RequestMapping(value ="/vertifyCode")
@ResponseBody
public Result<String> getVertifyCode(Model model, MiaoshaUser user,
@RequestParam("goodsId") Long goodsId, HttpServletResponse response) {
model.addAttribute("user", user);
//如果用户为空,则返回至登录页面
if(user==null){
return Result.error(CodeMsg.SESSION_ERROR);
}
BufferedImage img=miaoshaService.createMiaoshaVertifyCode(user, goodsId);
try {
OutputStream out=response.getOutputStream();
ImageIO.write(img,"JPEG", out);
out.flush();
out.close();
return null;
} catch (IOException e) {
e.printStackTrace();
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
}
/**
* 获取秒杀的path,并且验证验证码的值是否正确
*/
//@AccessLimit(seconds=5,maxCount=5,needLogin=true)
//加入注解,实现拦截功能,进而实现限流功能
//@AccessLimit(seconds=5,maxCount=5,needLogin=true)
@RequestMapping(value ="/getPath")
@ResponseBody
public Result<String> getMiaoshaPath(HttpServletRequest request,Model model,MiaoshaUser user,
@RequestParam("goodsId") Long goodsId,
@RequestParam(value="vertifyCode",defaultValue="0") int vertifyCode) {
model.addAttribute("user", user);
//如果用户为空,则返回至登录页面
if(user==null){
return Result.error(CodeMsg.SESSION_ERROR);
}
//限制访问次数
String uri=request.getRequestURI();
String key=uri+"_"+user.getId();
//限定key5s之内只能访问5次
Integer count=redisService.get(AccessKey.access, key, Integer.class);
if(count==null) {
redisService.set(AccessKey.access, key, 1);
}else if(count<5) {
redisService.incr(AccessKey.access, key);
}else {//超过5次
return Result.error(CodeMsg.ACCESS_LIMIT);
}
//验证验证码
boolean check=miaoshaService.checkVCode(user, goodsId,vertifyCode );
if(!check) {
return Result.error(CodeMsg.REQUEST_ILLEAGAL);
}
System.out.println("通过!");
//生成一个随机串
String path=miaoshaService.createMiaoshaPath(user,goodsId);
System.out.println("@MiaoshaController-tomiaoshaPath-path:"+path);
return Result.success(path);
}
/**
* 客户端做一个轮询,查看是否成功与失败,失败了则不用继续轮询。
* 秒杀成功,返回订单的Id。
* 库存不足直接返回-1。
* 排队中则返回0。
* 查看是否生成秒杀订单。
*/
@RequestMapping(value = "/result", method = RequestMethod.GET)
@ResponseBody
public Result<Long> doMiaoshaResult(Model model, MiaoshaUser user,
@RequestParam(value = "goodsId", defaultValue = "0") long goodsId) {
long result=miaoshaService.getMiaoshaResult(user.getId(),goodsId);
System.out.println("轮询 result:"+result);
return Result.success(result);
}
/**
* 563.1899076368552
* 做缓存+消息队列
* 1.系统初始化,把商品库存数量加载到Redis上面来。
* 2.收到请求,Redis预减库存。
* 3.请求入队,立即返回排队中。
* 4.请求出队,生成订单,减少库存(事务)。
* 5.客户端轮询,是否秒杀成功。
*
* 不能是GET请求,GET
*/
//POST请求
@RequestMapping(value="/{path}/do_miaosha_ajaxcache",method=RequestMethod.POST)
@ResponseBody
public Result<Integer> doMiaoshaCache(Model model,MiaoshaUser user,
@RequestParam(value="goodsId",defaultValue="0") long goodsId,
@PathVariable("path")String path) {
model.addAttribute("user", user);
//1.如果用户为空,则返回至登录页面
if(user==null){
return Result.error(CodeMsg.SESSION_ERROR);
}
//验证path,去redis里面取出来然后验证。
boolean check=miaoshaService.checkPath(user,goodsId,path);
if(!check) {
return Result.error(CodeMsg.REQUEST_ILLEAGAL);
}
//内存标记,减少对redis的访问 localMap.put(goodsId,false);
// boolean over=localMap.get(goodsId);
// //在容量满的时候,那么就打标记为true
// if(over) {
// return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);
// }
//2.预减少库存,减少redis里面的库存
long stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
//3.判断减少数量1之后的stock,区别于查数据库时候的stock<=0
if(stock<0) {
return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);
}
//4.判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId);
if (order != null) {// 重复下单
// model.addAttribute("errorMessage", CodeMsg.REPEATE_MIAOSHA);
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//5.正常请求,入队,发送一个秒杀message到队列里面去,入队之后客户端应该进行轮询。
MiaoshaMessage mms=new MiaoshaMessage();
mms.setUser(user);
mms.setGoodsId(goodsId);
mQSender.sendMiaoshaMessage(mms);
//返回0代表排队中
return Result.success(0);
}
/**
* 1000*10
* QPS 703.4822370735138
* @param model
* @param user
* @return
*/
@RequestMapping("/do_miaosha")
public String toList(Model model,MiaoshaUser user,@RequestParam("goodsId") Long goodsId) {
model.addAttribute("user", user);
//如果用户为空,则返回至登录页面
if(user==null){
return "login";
}
GoodsVo goodsvo=goodsService.getGoodsVoByGoodsId(goodsId);
//判断商品库存,库存大于0,才进行操作,多线程下会出错
int stockcount=goodsvo.getStockCount();
if(stockcount<=0) {//失败 库存至临界值1的时候,此时刚好来了加入10个线程,那么库存就会-10
model.addAttribute("errorMessage", CodeMsg.MIAOSHA_OVER_ERROR);
return "miaosha_fail";
}
//判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品
MiaoshaOrder order=orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(),goodsId);
if(order!=null) {//重复下单
model.addAttribute("errorMessage", CodeMsg.REPEATE_MIAOSHA);
return "miaosha_fail";
}
//可以秒杀,原子操作:1.库存减1,2.下订单,3.写入秒杀订单--->是一个事务
OrderInfo orderinfo=miaoshaService.miaosha(user,goodsvo);
//如果秒杀成功,直接跳转到订单详情页上去。
model.addAttribute("orderinfo", orderinfo);
model.addAttribute("goods", goodsvo);
return "order_detail";//返回页面login
}
/**
*
* 做了页面静态化的,直接返回订单的信息
* @param model
* @param user
* @param goodsId
* @return
*
* 不能是GET请求,GET,
*/
//POST请求
@RequestMapping(value="/do_miaosha_ajax",method=RequestMethod.POST)
@ResponseBody
public Result<OrderInfo> doMiaosha(Model model,MiaoshaUser user,@RequestParam(value="goodsId",defaultValue="0") long goodsId) {
model.addAttribute("user", user);
System.out.println("do_miaosha_ajax");
System.out.println("goodsId:"+goodsId);
//如果用户为空,则返回至登录页面
if(user==null){
return Result.error(CodeMsg.SESSION_ERROR);
}
GoodsVo goodsvo=goodsService.getGoodsVoByGoodsId(goodsId);
//判断商品库存,库存大于0,才进行操作,多线程下会出错
int stockcount=goodsvo.getStockCount();
if(stockcount<=0) {//失败 库存至临界值1的时候,此时刚好来了加入10个线程,那么库存就会-10
//model.addAttribute("errorMessage", CodeMsg.MIAOSHA_OVER_ERROR);
return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);
}
//判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品
MiaoshaOrder order=orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(),goodsId);
if(order!=null) {//重复下单
//model.addAttribute("errorMessage", CodeMsg.REPEATE_MIAOSHA);
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//可以秒杀,原子操作:1.库存减1,2.下订单,3.写入秒杀订单--->是一个事务
OrderInfo orderinfo=miaoshaService.miaosha(user,goodsvo);
//如果秒杀成功,直接跳转到订单详情页上去。
model.addAttribute("orderinfo", orderinfo);
model.addAttribute("goods", goodsvo);
return Result.success(orderinfo);
}
}
RabbitMQ:创建消息生产类、消息消费类、信息交换类
Redis:将秒杀对象初始化到内存中,设置对象



到这里秒杀流程就完成了。