最近学习多线程:
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>
<!-- <!– 缓存 –>-->
<!-- <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);
}
}
}