多线程实战---秒杀

 

最近学习多线程:

https://www.bilibili.com/video/BV1m4411u7xK/?p=74

仿着老师的思路,结合spring cloud 实现了一个简单的秒杀功能。

使用到的一些技术

1.redis 缓存

2.线程池

3.spring cloud feign  ,eureka  , config  ,定时任务 实现前后端分离

4.angularjs 

5.thymeleaf

6.mybatis逆向工程生成代码  参考别人的文档实现 

https://blog.csdn.net/qq_39056805/article/details/80585941

秒杀系统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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wang</groupId>
    <artifactId>seckill</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>seckill</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

<!--        &lt;!&ndash; 缓存 &ndash;&gt;-->
<!--        <dependency>-->
<!--            <groupId>redis.clients</groupId>-->
<!--            <artifactId>jedis</artifactId>-->
<!--            <version>2.8.1</version>-->
<!--        </dependency>-->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.28</version>
        </dependency>

        <dependency>
            <groupId>com.wang</groupId>
            <artifactId>entity</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>


        <!--        完善监控信息-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <version>1.5.19.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>1.5.3.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>



        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>1.5.3.RELEASE</version>
        </dependency>


        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>



        <!--        eureka配置-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
            <version>1.4.7.RELEASE</version>
        </dependency>

        <!--        对应config-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
            <version>1.4.7.RELEASE</version>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

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

</project>

秒杀系统后台项目结构

 

启动类

package com.wang.seckill;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
//开启定时任务支持
@EnableScheduling
public class SeckillApplication {

    public static void main(String[] args) {
        SpringApplication.run(SeckillApplication.class, args);
    }

}

 

controller

package com.wang.seckill.controller;


import com.fasterxml.jackson.databind.ser.std.TokenBufferSerializer;
import com.wang.blog.pojo.GoodAndUser;
import com.wang.seckill.pojo.SecKillResult;
import com.wang.seckill.pojo.TbSeckillGoods;
import com.wang.seckill.service.SeckillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
public class SeckillController {


    @Autowired
    SeckillService seckillService;


    @RequestMapping("/hello")
    @ResponseBody
    public String hello(){
        return "hello seckill";
    }

    @RequestMapping("/seckill/findAll")
    @ResponseBody
    public List<TbSeckillGoods> exeSelect(){
        List<TbSeckillGoods> list = seckillService.exeSelect();
        return list;
    }


    @RequestMapping("/select/mysql")
    @ResponseBody
    public List<TbSeckillGoods> exeSelectFromDataBase(){
        List<TbSeckillGoods> list = seckillService.exeSelectFromDataBase();
        return list;
    }

    @RequestMapping("/seckill/findOne/{id}")
    @ResponseBody
    public TbSeckillGoods findOne(@PathVariable("id") long id){
        return seckillService.findOne(id) ;
    }

    @PostMapping("/seckill/saveOrder")
    @ResponseBody
    public SecKillResult saveOrder(@RequestBody GoodAndUser goodAndUser){
        return seckillService.saveOrder(goodAndUser);
    }
}

 

 

serviceimpl

秒杀核心功能:

创建订单之前会先判断是否有订单未支付

再判断库存

都判断成功然后才使用线程池创建订单

package com.wang.seckill.service;


import com.wang.blog.pojo.GoodAndUser;
import com.wang.seckill.mapper.TbSeckillGoodsMapper;
import com.wang.seckill.pojo.SecKillResult;
import com.wang.seckill.pojo.TbSeckillGoods;
import com.wang.seckill.pojo.TbSeckillGoodsExample;
import com.wang.seckill.pojo.TbSeckillOrder;
import com.wang.seckill.thread.CreateOrderThread;
import com.wang.utils.Constant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.client.RestTemplate;

import javax.xml.crypto.Data;
import java.util.Date;
import java.util.List;

@Service
public class SeckillServiceImpl implements  SeckillService {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private TbSeckillGoodsMapper tbSeckillGoodsMapper;

    @Autowired
    private IdWorker idWorker;
    @Autowired
    private ThreadPoolTaskExecutor executor;
    @Autowired
    CreateOrderThread createOrderThread;
    @Override
    public List<TbSeckillGoods> exeSelectFromDataBase() {
        TbSeckillGoodsExample example = new TbSeckillGoodsExample();
        example.createCriteria()
                .andStartTimeLessThan(new Date())
                .andEndTimeGreaterThan(new Date())
                .andStatusEqualTo("1")
                .andStockCountGreaterThan(0);
        return tbSeckillGoodsMapper.selectByExample(example);
    }

    /**
     * 同步数据库的数据到redis
     * 此处使用的定时任务,cron
     */
    @Override
    @Scheduled(cron = "0 10 08 02 05 *")
    public void synchDataToRedis() {
        TbSeckillGoodsExample example = new TbSeckillGoodsExample();
        //查询秒杀商品  当前时间在开始时间和结束时间中间
        //状态为1
        //数量大于0
        example.createCriteria()
                .andStartTimeLessThan(new Date())
                .andEndTimeGreaterThan(new Date())
                .andStatusEqualTo("1")
                .andStockCountGreaterThan(0);
        System.out.println("定时执行"+new Date());
        for (TbSeckillGoods tbSeckillGood : tbSeckillGoodsMapper.selectByExample(example)) {
//            写数据到redis
            redisTemplate.opsForHash().put(TbSeckillGoods.class.getSimpleName(),tbSeckillGood.getId(),tbSeckillGood);
            //为了解决多线程超卖问题,此处同时把商品放一份队列 因为队列里面有多少,就限制多少线程取 就不会出现超卖
             createGoodsQueue(tbSeckillGood);

        }

    }

    /**
     * 根据商品数量存一份商品信息队列
     * @param tbSeckillGood
     */
    private void createGoodsQueue(TbSeckillGoods tbSeckillGood) {
        if(tbSeckillGood.getStockCount()>0){
//            数量大于0
            for (Integer i = 0; i < tbSeckillGood.getStockCount(); i++) {
                redisTemplate.opsForList().leftPush(Constant.SECKILL_GOODS_PREFIX +tbSeckillGood.getId(),tbSeckillGood.getId());
            }
        }
    }

    /**
     * 查询所有秒杀商品
     * @return
     */
    @Override
    public List<TbSeckillGoods> exeSelect() {
        return redisTemplate.opsForHash().values(TbSeckillGoods.class.getSimpleName());
    }

    /**
     * 根据id从redis查到商品返回
     * @param id
     * @return
     */
    public TbSeckillGoods findOne(long id) {
        return (TbSeckillGoods) redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).get(id);
    }

    /**
     * 这个方法解决多线程存在超卖问题
     * 并使用线程池提升效率
     * @param goodAndUser
     * @return
     */
    @Override
    public SecKillResult saveOrder(GoodAndUser goodAndUser) {


//        下订单之前,应该判断set是否有未支付的订单
        //某一个商品是否已经被该用户下过单
        if (redisTemplate.boundSetOps(Constant.SECKILL_USER_ID_PREFIX+goodAndUser.getId())
                .isMember(goodAndUser.getUserId())) {
//            是,说明该商品该用户有订单等待支付 不再创建订单直接返回
            return new SecKillResult(false,"您有未支付的订单,请先支付");
        }
        //从商品队列中判断 //        判断商品是否存在或者售完
        if (redisTemplate.boundListOps(Constant.SECKILL_GOODS_PREFIX+goodAndUser.getId())==null) {
//            售罄  不再创建订单 直接返回
            return  new SecKillResult(false,"商品已被抢完,请看看其他商品");
        }else {
//            用户信息放入用户set
            redisTemplate.boundSetOps(Constant.SECKILL_USER_ID_PREFIX+goodAndUser.getId()).add(goodAndUser.getUserId());
            //redis中放一个订单队列,存放的就是GoodAndUser信息
            redisTemplate.boundListOps(GoodAndUser.class.getSimpleName()).leftPush(goodAndUser);
            //创建订单,多线程异步执行
            executor.submit(createOrderThread);
        }
        return new SecKillResult(true,"恭喜,抢购成功,请尽快完成支付");

    }

    @Override
    public SecKillResult saveOrderForMulti(GoodAndUser goodAndUser) {
        return null;
    }


    /**
     * 这个方法多线程存在超卖问题
     *
     * @param goodAndUser
     * @return
     */
//    @Override
//    public SecKillResult saveOrder(GoodAndUser goodAndUser) {
//
        从redis获得商品数据
//        TbSeckillGoods tbSeckillGoods = findOne(goodAndUser.getId());
        判断商品是否存在或者售完
//        if (tbSeckillGoods==null || tbSeckillGoods.getStockCount()<=0) {
        是 结束
//            return  new SecKillResult(false,"商品已被抢完,请看看其他商品");
//        }else {
        否创建秒杀订单,库存是否<0  否更新redis  是存数据库,删除redis缓存 结束
//            TbSeckillOrder tbSeckillOrder = new TbSeckillOrder();
//            //idworker  id生成器,可以减少id碰撞
//            tbSeckillOrder.setSeckillId(idWorker.nextId());
//            tbSeckillOrder.setSellerId(tbSeckillGoods.getSellerId());
//            tbSeckillOrder.setCreateTime(new Date());
//            tbSeckillOrder.setMoney(tbSeckillGoods.getCostPrice());
//            tbSeckillOrder.setUserId(goodAndUser.getUserId());
//
            秒杀订单存入缓存,商品数量减一
//            redisTemplate.opsForHash().put(tbSeckillOrder.getClass().getSimpleName(),tbSeckillOrder.getUserId(),tbSeckillOrder);
//            tbSeckillGoods.setStockCount(tbSeckillGoods.getStockCount()-1);
//            if(tbSeckillGoods.getStockCount()>0){
                减完后还有商品 更新缓存
//                redisTemplate.opsForHash().put(TbSeckillGoods.class.getSimpleName(),tbSeckillGoods.getId(),tbSeckillGoods);
//            }else{
                没有商品删除缓存,更新数据库
//                redisTemplate.opsForHash().delete(tbSeckillGoods.getClass().getSimpleName(),tbSeckillGoods.getId());
//                tbSeckillGoodsMapper.updateByPrimaryKey(tbSeckillGoods);
//            }
//        }
//        return new SecKillResult(true,"恭喜,抢购成功,请尽快完成支付");
//
//    }

}

thread

package com.wang.seckill.thread;

import com.wang.blog.pojo.GoodAndUser;
import com.wang.seckill.mapper.TbSeckillGoodsMapper;
import com.wang.seckill.pojo.TbSeckillGoods;
import com.wang.seckill.pojo.TbSeckillOrder;
import com.wang.seckill.service.IdWorker;
import com.wang.utils.Constant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.swing.border.TitledBorder;
import java.util.Date;

@Component
public class CreateOrderThread implements Runnable {

    @Autowired
    private TbSeckillGoodsMapper tbSeckillGoodsMapper;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    IdWorker idWorker;
    @Override
    public void run() {
        //            从redis获得商品数据 goodId和userId  相当于之前已经预备待创建的订单
        GoodAndUser goodAndUser = (GoodAndUser) redisTemplate.boundListOps(GoodAndUser.class.getSimpleName()).rightPop();
        if (goodAndUser!=null) {
            TbSeckillGoods tbSeckillGoods = (TbSeckillGoods) redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).get(goodAndUser.getId());
            // 创建秒杀订单,之后判断库存是否<0  否更新redis  是存数据库,删除redis缓存 结束
            TbSeckillOrder tbSeckillOrder = new TbSeckillOrder();
            //idworker  id生成器,可以减少id碰撞
            tbSeckillOrder.setSeckillId(idWorker.nextId());
            tbSeckillOrder.setSellerId(tbSeckillGoods.getSellerId());
            tbSeckillOrder.setCreateTime(new Date());
            tbSeckillOrder.setMoney(tbSeckillGoods.getCostPrice());
            tbSeckillOrder.setUserId(goodAndUser.getUserId());

//            秒杀订单存入缓存,商品数量减一  多线程,需要同步
            synchronized (TitledBorder.class){
                tbSeckillGoods = (TbSeckillGoods) redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).get(goodAndUser.getId());
                redisTemplate.opsForHash().put(tbSeckillOrder.getClass().getSimpleName(),tbSeckillOrder.getUserId(),tbSeckillOrder);
                tbSeckillGoods.setStockCount(tbSeckillGoods.getStockCount()-1);
                if(tbSeckillGoods.getStockCount()>0){
//                减完后还有商品 更新缓存
                    redisTemplate.opsForHash().put(TbSeckillGoods.class.getSimpleName(),tbSeckillGoods.getId(),tbSeckillGoods);
                    redisTemplate.boundListOps(Constant.SECKILL_GOODS_PREFIX+tbSeckillGoods.getId()).rightPop();
                }else{
//                没有商品删除缓存,更新数据库
                    redisTemplate.opsForHash().delete(tbSeckillGoods.getClass().getSimpleName(),tbSeckillGoods.getId());
                    tbSeckillGoodsMapper.updateByPrimaryKey(tbSeckillGoods);
                }
            }
        }
    }
}

实体类

单独有一个module放实体类,准确的说是api   ,在这个module下面配置了entity,也有生成id,线程池等工具。

IdWorker

是一个id生成工具,避免主键冲突

ThreadPoolConfig

线程池

package com.wang.seckill.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 线程池配置
 */
@Configuration
public class ThreadPoolConfig  {
    @Bean
    RejectedExecutionHandler rejectedExecutionHandler(){
        return new ThreadPoolExecutor.CallerRunsPolicy();
    }

    @Bean("executor")
    ThreadPoolTaskExecutor threadPoolTaskExecutor(RejectedExecutionHandler rejectedExecutionHandler){
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
//        核心线程数
        threadPoolTaskExecutor.setCorePoolSize(10);
//        最大线程数 默认为Integer.MAX_VALUE
        threadPoolTaskExecutor.setMaxPoolSize(50);
//        队列的最大长度,一般需要设置值>=notifyScheduledMainExecutor.maxNum;默认为Integer.MAX_VALUE
        threadPoolTaskExecutor.setQueueCapacity(10000);
//        线程池维护线程所允许的空闲时间,默认60s
        threadPoolTaskExecutor.setKeepAliveSeconds(300);
//        线程池,无线程可用时处理策略,ThreadPoolExecutor.AbortPolicy  CallerRunsPolicy(默认)
        threadPoolTaskExecutor.setRejectedExecutionHandler(rejectedExecutionHandler);
        return threadPoolTaskExecutor;
    }
}
/*
*
*
* CallerRunsPolicy :
这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功。
AbortPolicy :
对拒绝任务抛弃处理,并且抛出异常。
DiscardPolicy :
对拒绝任务直接无声抛弃,没有异常信息。
DiscardOldestPolicy :
对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个线程,然后把拒绝任务加到队列。

* */

 

 

bootstrap.yml

spring:
  cloud:
    config:  #作为config客户端,从config server读配置
      name: seckillplatform   #从gitee上面读取的资源名称
      label: master
      profile: dev
      uri: http://localhost:3344   #注意这里不是直连git,不应该再出现git

主配置文件

 

测试结果:

秒杀商品展示:

 

 

redis查看结果

 

MySQL too many connections错误

Caused by: java.sql.SQLNonTransientConnectionException: Data source rejected establishment of connection,  message from server: "Too many connections"

-- 查看最大连接数

SHOW VARIABLES LIKE 'max_connections';
-- 查看线程睡眠时间
SHOW GLOBAL VARIABLES LIKE 'wait_timeout' 
-- 设置最大连接数
SET GLOBAL max_connections=1000;
-- 设置睡眠时间
SET GLOBAL wait_timeout=300; 

 

参考:https://jingyan.baidu.com/article/fc07f989c5c6bd52fee5192c.html

 

redis序列化问题

可以看到,同步到redis成功,但是数据不太看得懂:  这个不影响查询。

 

问题 写数据到redis 报错

 

org.springframework.dao.InvalidDataAccessApiUsageException: READONLY You can't write against a read only replica.; nested exception is redis.clients.jedis.exceptions.JedisDataException: READONLY You can't write against a read only replica.
这是因为我之前配置了redis的哨兵模式,此时我是用的6379端口,但是该端口已经被设置为从机,从机只能读,不能写。

package com.wang.seckill.service;


import com.wang.seckill.mapper.TbSeckillGoodsMapper;
import com.wang.seckill.pojo.TbSeckillGoods;
import com.wang.seckill.pojo.TbSeckillGoodsExample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import javax.xml.crypto.Data;
import java.util.Date;

@Service
public class SeckillServiceImpl implements  SeckillService {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private TbSeckillGoodsMapper tbSeckillGoodsMapper;

    @Override
    @Scheduled(cron = "0/10 * * * * *")
    public void synchDataToRedis() {
        TbSeckillGoodsExample example = new TbSeckillGoodsExample();
        example.createCriteria()
                .andStartTimeLessThan(new Date())
                .andEndTimeGreaterThan(new Date())
                .andStatusEqualTo("1")
                .andStockCountGreaterThan(0);
        System.out.println("定时执行"+new Date());
        for (TbSeckillGoods tbSeckillGood : tbSeckillGoodsMapper.selectByExample(example)) {
//            写数据到redis
            redisTemplate.opsForHash().put("SeckillGoods",tbSeckillGood.getId(),tbSeckillGood);
        }

    }
}

 

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值