并发业务可以说现在随处可见了,比如我们在淘宝上的秒杀,微信往群里放一个红包,这些都是属于并发,而且在短时间内会有很大的请求到后台。传统的系统流程一般都是直接一步走完,将很大的请求都压在了数据库上,分分钟导致系统崩溃或请求响应速度过慢。
在看看我们秒杀或者是微信红包,我们在点击后,都可能有个温馨提示。秒杀的可能说什么正在排队,微信红包的则在转圈圈。后台的实质是在异步处理你的订单或者你的红包请求入库操作。
花了几天时间根据这个流程写了个并发demo,大致上完成了一部分功能需求,真正的秒杀可能很复杂。这里仅供学习参考。当然也存在一个很郁闷的bug,就是会出现少卖现象,超卖现象不存在(原因可以参考代码)。
win10+IntelliJ IDEA +JDK1.8+redis3.5+rabbitmq(版本忘记了)+Mybatis
springboot版本:springboot 1.5.14
https://github.com/LuckyToMeet-Dian-N/myseckill
(1)搭建redis环境,这里接不做介绍了。如果还没搭建的同学参考我的另一篇博客:随笔(三)-- linux下安装redis
(2)搭建rabbitmq环境,这里也不做介绍,没有安装的同学请参考:随笔(五) rabbitmq的安装与配置
按照步骤就可以装成功了。
pom.xml
4.0.0
com.wen
seckill
0.0.1-SNAPSHOT
jar
seckill
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
1.5.14.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-amqp
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
org.springframework.boot
spring-boot-starter-aop
org.projectlombok
lombok
true
redis.clients
jedis
2.7.3
com.alibaba
fastjson
1.2.47
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
application.properties
#MYSQL
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=wuxiwen
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#时区设置
spring.jackson.time-zone=GMT+8
#日期期时格式设置置
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
# redis
redis.host=reids的ip
redis.port=6379
redis.timeout=10
redis.poolMaxTotal=1000
redis.poolMaxIdle=500
redis.poolMaxWait=500
#rabbitmq
spring.rabbitmq.host=自己的mqip
spring.rabbitmq.port=5672
spring.rabbitmq.username=Gentle
spring.rabbitmq.password=123456
秒杀流程:
* 流程:
* 判断商品是否存在
* 判断本地内存中商品是否还有
* 判断是否重复秒杀
* 判断redis是否还有内存
* 将秒杀信息交给mq做异步下单
* mq处再次判断是否重单以及判断商品是否还有库存
* 最后就是减库存,下订单(使用乐观锁)
秒杀有关的Controller:
package com.wen.seckill.controller;
import com.wen.seckill.result.ResultBean;
import com.wen.seckill.service.SeckillService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description:
* @Author: Gentle
* @date 2018/9/18 16:06
*/
@RestController
@Slf4j
public class SeckillController {
@Autowired
SeckillService seckillService;
/**
* 秒杀,只允许传一个商品号来,就不做各种限制操作
*
* @param productId
* @return
*/
@RequestMapping(value = "seckill")
public ResultBean doSeckill(@RequestParam("productId") long productId) {
long a = System.currentTimeMillis();
ResultBean resultBean = new ResultBean<>(seckillService.doSeckill(productId));
log.info("花费时间:" + (System.currentTimeMillis() - a));
return resultBean;
}
/**
* 初始化商品。初始化秒杀的商品。默认商品的id 是 1 和 2
* @return
*/
@RequestMapping(value = "resetProduct")
public ResultBean reset() {
return new ResultBean<>(seckillService.reSetProduct());
}
}
秒杀有关的service:
主要是将用户请求的各种数据进行判断,并预减库存,减少大部分线程进入到系统下游(数据库层),导致数据库崩溃。
使用redis的目的除了能顶住很大的并发量(单机版能处理1W的并发),还有就是redis自增自减操作十分诱人。
使用本地内存标记的目的除了查看不正确的请求,也有减少线程过度的访问redis导致连接超时问题。
使用mq目的是进行异步下单操作,除了增加用户体验,让用户更快知道自己抢购后的状态。
package com.wen.seckill.service.Impl;
import com.wen.seckill.constant.Constants;
import com.wen.seckill.domian.SeckillOrder;
import com.wen.seckill.exception.CheckException;
import com.wen.seckill.rabbitmq.MQSender;
import com.wen.seckill.rabbitmq.SeckillMessage;
import com.wen.seckill.redis.RedisService;
import com.wen.seckill.service.OrdersService;
import com.wen.seckill.service.ProductService;
import com.wen.seckill.service.SeckillService;
import com.wen.seckill.utils.JsonUtils;
import com.wen.seckill.utils.RequestAndResponseUtils;
import com.wen.seckill.vo.OrderInfo;
import com.wen.seckill.vo.Product;
import com.wen.seckill.vo.Users;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Description:
* @Author: Gentle
* @date 2018/9/17 13:55
*/
@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {
AtomicInteger atomicInteger= new AtomicInteger();
@Autowired
RedisService redisService;
@Autowired
ProductService productService;
@Autowired
OrdersService ordersService;
@Autowired
MQSender mqSender;
//本地内存,立刻更新内存标记,让所有线程立即可见。单机版的秒杀可以这么玩,分布式环境下就别这么玩了
private volatile Map map = new HashMap<>();
/**
* 流程:
* 判断商品是否存在
* 判断本地内存中商品是否还有
* 判断是否重复秒杀
* 判断redis是否还有内存
* 将秒杀信息交给mq做异步下单
* @param productId
* @return
*/
@Override
public String doSeckill(long productId) {
//这个是线程安全的
HttpServletRequest request= RequestAndResponseUtils.getRequest();
Users users = (Users) request.getAttribute(Constants.USER_SESSION);
//判断本地内存中是否有这件商品
boolean flag= map.containsKey(productId);
if (!flag){
throw new CheckException("商品不存在");
}
//本地内存标记,减少redis的访问,单机版的秒杀可以这么玩,分布式环境下就别这么玩了
boolean temp= (boolean) map.get(productId);
if (temp){
throw new CheckException("商品抢购完毕~!");
}
//查看是否重复秒杀了
SeckillOrder repeatSeckill = ordersService.isRepeatSeckill(users.getUser_id(), productId);
if (repeatSeckill!=null){
throw new CheckException("请不要重复秒杀");
}
//访问redis,并将商品的值减1
long number =redisService.decr(Constants.SECKILL_PRODECT+":"+productId);
//判断redis内商品数量,抢购完毕改变一个本地内存标记
if (number<0){
map.put(productId,true);
throw new CheckException("商品被抢完了~!!!!");
}
SeckillMessage seckillMessage = new SeckillMessage();
seckillMessage.setProduct_id(productId);
seckillMessage.setUsers(users);
//将信息交给Rabbit队列处理订单,rabbitmq异步处理下单流程
mqSender.sendMiaoshaMessage(JsonUtils.ObjectTojson(seckillMessage));
return "OK";
}
@Override
public String reSetProduct() {
log.info("开始");
List allSeckillPrroduct = productService.findAllSeckillPrroduct();
System.out.println(allSeckillPrroduct);
if (allSeckillPrroduct.isEmpty()){
log.info("插入");
Product product =new Product();
product.setNumber(10);
product.setDescription("就是那么帅");
product.setProduct_name("Gentle");
for (int i=1;i<3;i++){
product.setProduct_id((long)i);
productService.insertProductInfo(product);
}
return reSetProduct();
}
log.info("查询");
for (Product product :allSeckillPrroduct){
productService.updateProductInfo(product);
//秒杀的商品的id
redisService.set(Constants.SECKILL_PRODECT+":"+product.getProduct_id(),product.getNumber());
//放入map,本地内存抗压
map.put(product.getProduct_id(),false);
}
return "OK";
}
}
异步下单的rabbitmq接收器:
package com.wen.seckill.rabbitmq;
import com.wen.seckill.domian.SeckillOrder;
import com.wen.seckill.exception.CheckException;
import com.wen.seckill.redis.RedisService;
import com.wen.seckill.service.OrdersService;
import com.wen.seckill.service.ProductService;
import com.wen.seckill.utils.JsonUtils;
import com.wen.seckill.vo.Product;
import com.wen.seckill.vo.Users;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
/**
* @Description: rabbitmq接收者,接收发送者的信息
* @Author: Gentle
* @date 2018/9/19 16:01
*/
@Component
@Slf4j
public class MQReceiver {
@Autowired
RedisService redisService;
@Autowired
ProductService productService;
@Autowired
OrdersService ordersService;
/* 此处启动会报错,需要在http://localhost:15672增加miaosha.queue的对列*/
@RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)
public void receiveMiaoshaMessage(String message) {
log.info("rabbitmq接收消息");
SeckillMessage seckillMessage = JsonUtils.jsonToObject(message,SeckillMessage.class);
Users users = seckillMessage.getUsers();
Product productInfo = productService.getProductInfo(seckillMessage.getProduct_id());
/**
* 判断商品数量是否符合
*/
if (productInfo.getNumber() <= 0) {
log.info("商品抢购已经完毕1");
return;
}
/**
* 在此判断是否重复秒杀
*/
SeckillOrder repeatSeckill1 = ordersService.isRepeatSeckill(users.getUser_id(), seckillMessage.getProduct_id());
if (repeatSeckill1 != null) {
log.info("商品抢购已经完毕1,请不要重复秒杀");
return;
}
//减库存 下订单
ordersService.doSeckill(productInfo, users);
}
}
秒杀下单service:
前面已经对数据进行各种校验,这里直接将所需要的数据插入数据库即可。
package com.wen.seckill.service.Impl;
import com.wen.seckill.constant.Constants;
import com.wen.seckill.domian.SeckillOrder;
import com.wen.seckill.exception.CheckException;
import com.wen.seckill.factory.OrdersFactory;
import com.wen.seckill.mapper.OrdersMapper;
import com.wen.seckill.redis.RedisService;
import com.wen.seckill.service.OrdersService;
import com.wen.seckill.utils.JsonUtils;
import com.wen.seckill.utils.Sequence;
import com.wen.seckill.vo.OrderInfo;
import com.wen.seckill.vo.Product;
import com.wen.seckill.vo.Users;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cglib.beans.BeanCopier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import sun.applet.Main;
/**
* @Description: 订单操作
* @Author: Gentle
* @date 2018/9/17 13:55
*/
@Service
public class OrdersServiceImpl implements OrdersService {
@Autowired
RedisService redisService;
@Autowired
OrdersMapper ordersMapper;
/**
* 判断是否重复下单
* @param userId
* @param productId
* @return
*/
@Override
public SeckillOrder isRepeatSeckill(int userId, long productId) {
String value = redisService.get(Constants.USER_PRODUCT_BY_SECKILL + ":" + userId + "_" + productId);
if (value != null) {
return JsonUtils.jsonToObject(value, SeckillOrder.class);
}
return null;
}
/**
* 减少库存数量
* @param productId
* @param version
* @return
*/
@Override
public boolean reduceProductNumber(long productId, int version) {
int temp = ordersMapper.reduceProductNumber(productId, version);
if (temp != 1) {
return false;
}
return true;
}
@Override
@Transactional
public SeckillOrder doSeckill(Product product, Users users) {
System.out.println(product +" "+users);
boolean flag= reduceProductNumber(product.getProduct_id(),product.getVersion());
if (flag){
//获取单例对象生成订单。
Sequence sequence =OrdersFactory.getInstance();
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setProduct_id(product.getProduct_id());
seckillOrder.setOrder_id(sequence.nextId());
seckillOrder.setUser_id(users.getUser_id());
//插入数据库
insertOrder(seckillOrder);
//存入redis
String mess= JsonUtils.ObjectTojson(seckillOrder);
//放入redis,用于判断是否重复下单
redisService.set(Constants.USER_PRODUCT_BY_SECKILL + ":" + users.getUser_id() + "_" + product.getProduct_id(),mess);
return seckillOrder;
}else {
throw new CheckException("商品售完了~");
}
}
/**
* 抢购订单入库
* @param seckillOrder
* @return
*/
@Override
public int insertOrder(SeckillOrder seckillOrder) {
return ordersMapper.insertOrder(seckillOrder);
}
}
以上代码为核心代码,具体还有很多其他的操作以及各种我封装好的工具类。博客仅做思路解析,代码不完整,完整代码请到GitHub下载学习。
(1)想将seckillsql文件导入数据库,之后改properties文件中的属性
(2)项目发布好后,需要先调用接口:
http://localhost:8080/resetProduct 目的是初始化商品,初始商品信息以及模拟用户信息
(3) 调用秒杀接口:http://localhost:8080/seckill?productId=2 productId=1或是2都是可以的。。默认设置2个商品。
该demo有很多不足之处,当然也有很多可以学习的地方,比如:设计模式,数据返回规范,异常处理等,这个需要靠我们自己去看去了解了。 因为是简单模拟并发,就多附带的功能都没有去实现,比如安全校验,ip限制,验证码校验这些都是需要做的。这里就真的不去做了。至于少卖问题,需要请教大神更好的策略,当然也可以选择分布式锁来解决。如果有好的并发demo,欢迎@我,希望大家不吝赐教。祝大家学习进步,工作顺利。
--谢谢~!