项目demo(一)--springboot并发抢购项目demo

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_41622183/article/details/82820484

前言:

         并发业务可以说现在随处可见了,比如我们在淘宝上的秒杀,微信往群里放一个红包,这些都是属于并发,而且在短时间内会有很大的请求到后台。传统的系统流程一般都是直接一步走完,将很大的请求都压在了数据库上,分分钟导致系统崩溃或请求响应速度过慢。

        在看看我们秒杀或者是微信红包,我们在点击后,都可能有个温馨提示。秒杀的可能说什么正在排队,微信红包的则在转圈圈。后台的实质是在异步处理你的订单或者你的红包请求入库操作。

        花了几天时间根据这个流程写了个并发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

<?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.wen</groupId>
	<artifactId>seckill</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>seckill</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.14.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>


		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.7.3</version>
		</dependency>


		<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.47</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>


</project>

             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<String> 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<String> 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<Long,Object> 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<Product> 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,欢迎@我,希望大家不吝赐教。祝大家学习进步,工作顺利。                                                                                                      

                                                                                                                                                                              --谢谢~!

程序人生,与君共勉~!

猜你喜欢

转载自blog.csdn.net/weixin_41622183/article/details/82820484
今日推荐