秒杀业务是怎么做的?
一、前言
一提到秒杀,都会想到高性能、高并发、高可用、大流量…。在电商体系中,交易系统占据了环节中的半壁江山。比如里面特别迷人的秒杀系统,那秒杀涉及到什么架构设计?会涉及到什么业务?
二、 秒杀业务难点
(1)高并发
用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。
(2)超卖
由于库存并发更新的问题,导致在实际库存已经不足的情况下,库存依然在减,导致卖家的商品卖得件数超过秒杀的预期。
三、项目实现
1、项目所用技术
SpringBoot2.x框架 、 maven仓库、 rabbitMQ队列、redis缓存
技术 | 具体 |
---|---|
开发工具 | idea |
所用框架 | SpringBoot2.2 |
消息中间 | rabbitMQ |
数据库 | MySQL |
缓存 | reids |
高并发压力测试工具 | jmeter |
dao层 | mybatis |
2、项目依赖
创建数据库
创建订单表order
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80018
Source Host : localhost:3306
Source Schema : db_concurrency
Target Server Type : MySQL
Target Server Version : 80018
File Encoding : 65001
Date: 23/12/2019 15:41:28
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for order
-- ----------------------------
DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '商品名称',
`order_user` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '用户名',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
创建stock 商品表
-- ----------------------------
-- Table structure for stock
-- ----------------------------
DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '商品名',
`stock` int(255) NULL DEFAULT NULL COMMENT '库存数量',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '商品表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of stock
-- ----------------------------
INSERT INTO `stock` VALUES (1, '小米手机', 83);
SET FOREIGN_KEY_CHECKS = 1;
pom文件
<?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>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com</groupId>
<artifactId>redis-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<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>2.1.1</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<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>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<includes>
<include>**/*Mapper.xml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
springBoot配置文件
spring:
rabbitmq:
virtual-host: /
host: localhost #rabbitMQ服务器ip
username: guest
password: guest
# listener:
# imple:
# acknowledge-mode: manual
# application:
# name: concurrency-project
redis:
host: 192.168.25.129 #redis服务器的ip
port: 6379
jedis:
pool:
max-active: 1024
max-wait: -1s
max-idle: 200
password: redis
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver #MySQLl8.0驱动
url: jdbc:mysql://localhost:3306/db_concurrency?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true # ?后面的参数是MySQL8.0连接必须加的,配置MySQL服务器的时区
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
druid: #druid数据源
initial-size: 1
min-idle: 1
max-active: 20
#获取连接等待超时时间
max-wait: 60000
#间隔多久进行一次检测,检测需要关闭的空闲连接
time-between-eviction-runs-millis: 60000
#一个连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
mybatis: #mybais配置
mapper-locations: mapper/*.xml
type-aliases-package: com.redisdemo.model
#pagehelper:
## helper-dialect: mysql
mapper:
mappers: com.redisdemo.mapper #dao层包的位置
not-empty: false
identity: MYSQL
3、rabbitMQ配置类 MyRabbitMQConfig
package com.redisdemo.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ配置类
*/
@Configuration
public class MyRabbitMQConfig {
/**
* 库存交换机
*/
public static final String STORY_EXCHANGE = "STORY_EXCHANGE";
/**
* 订单交换机
*/
public static final String ORDER_EXCHANGE = "ORDER_EXCHANGE";
/**
* 库存队列
*/
public static final String STORY_QUEUE = "STORY_QUEUE";
/**
* 订单队列
*/
public static final String ORDER_QUEUE = "ORDER_QUEUE";
/**
*库存路由键
*/
public static final String STORY_ROUTING_KEY = "STORY_ROUTING_KEY";
/**
*订单路由
*/
public static final String ORDER_ROUTING_KEY = "ORDER_ROUTING_KEY";
// @Bean
// public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
// SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
// factory.setConnectionFactory(connectionFactory);
// factory.setMessageConverter(new Jackson2JsonMessageConverter());
// factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// return factory;
// }
/**
* 创建MessageConverter对象
* @return
*/
@Bean
public MessageConverter messageConverter(){
//setAcknowledgeMode(AcknowledgeMode.MANUAL);
return new Jackson2JsonMessageConverter();
}
/**
* 创建库存交换机
* @return
*/
@Bean
public Exchange getStoryExchange(){
//构建器
ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(STORY_EXCHANGE);
//设置是否持久 true 或者 fasle
Exchange build = exchangeBuilder.durable(true).build();
return build;
}
/**
* 创建库存队列
* @return 队列
*/
@Bean
public Queue getStoryQueue(){
return new Queue(STORY_QUEUE);
}
/**
* 库存交换机和库存队列绑定
* @return
*/
@Bean
public Binding bindStory(){
return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs();
}
/**
* 订单队列
* @return
*/
@Bean
public Queue getOrderQueue(){
return new Queue(ORDER_QUEUE);
}
/**
* 创建订单交换机
* @return
*/
@Bean
public Exchange getOrderExchange(){
return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
}
/**
* 将订单队列和订单交换机进行绑定
* @return
*/
@Bean
public Binding bindOrder(){
return BindingBuilder.bind(getOrderQueue()).to(getOrderExchange()).with(ORDER_ROUTING_KEY).noargs();
}
}
4、创建redis配置类
package com.redisdemo.config;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis配置类
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
5、创建实体类
Order
package com.redisdemo.model;
import lombok.Data;
@Data //这是lombok插件的注解,如果没有的可以删除该注解,自己生成get、set方法
public class Order {
private Integer id;
private String orderName;
private String orderUser;
}
Stock
package com.redisdemo.model;
import lombok.Data;
@Data //这是lombok插件的注解,如果没有的可以删除该注解,自己生成get、set方法
public class Stock {
private Integer id;
private String name;
private Integer stock;
}
6、创建service
IOrderService
package com.redisdemo.service;
import com.redisdemo.model.Order;
import org.springframework.stereotype.Service;
public interface IOrderService {
void createOrder(Order order);
}
IRedisService
package com.redisdemo.service;
import java.util.Date;
public interface IRedisService {
/**
* 添加到redis
* @param key
* @param value
* @param milis
*/
void put(String key,Object value,long milis);
void putForHash(String objectKey,String hkey,String value);
<T>T get(String key,Class type);
void remove(String key);
boolean expire(String key,Long milis);
boolean persist(String key);
String getString(String key);
Integer getInter(String key);
Long getLong(String key);
Date getDate(String key);
Long decrBy(String key);
}
IStockService
package com.redisdemo.service;
public interface IStockService {
void decrByStock(String stockName);
Integer selectByExample(String stockName);
}
IOrderServiceImpl
package com.redisdemo.service.impl;
import com.redisdemo.mapper.OrderMapper;
import com.redisdemo.model.Order;
import com.redisdemo.service.IOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class IOrderServiceImpl implements IOrderService {
@Autowired
private OrderMapper orderMapper;
@Override
public void createOrder(Order order) {
orderMapper.insert(order);
}
}
IRedisServiceImpl
package com.redisdemo.service.impl;
import com.redisdemo.service.IRedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Service
public class IRedisServiceImpl implements IRedisService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 设置string键值对
* @param key 键
* @param value 值
* @param milis 时间
*/
@Override
public void put(String key, Object value, long milis) {
redisTemplate.opsForValue().set(key,value,milis,TimeUnit.MINUTES);
}
@Override
public void putForHash(String objectKey, String hkey, String value) {
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.put(objectKey,hkey,value);
}
@Override
public <T> T get(String key, Class type) {
return (T) redisTemplate.boundValueOps(key).get();
}
@Override
public void remove(String key) {
redisTemplate.delete(key);
}
@Override
public boolean expire(String key, Long milis) {
return redisTemplate.expire(key,milis, TimeUnit.MILLISECONDS);
}
@Override
public boolean persist(String key) {
return redisTemplate.hasKey(key);
}
@Override
public String getString(String key) {
return (String) redisTemplate.opsForValue().get(key);
}
@Override
public Integer getInter(String key) {
return (Integer) redisTemplate.opsForValue().get(key);
}
@Override
public Long getLong(String key) {
return (Long) redisTemplate.opsForValue().get(key);
}
@Override
public Date getDate(String key) {
return (Date) redisTemplate.opsForValue().get(key);
}
@Override
public Long decrBy(String key) {
return redisTemplate.opsForValue().decrement(key);
}
}
IStockServiceImpl
package com.redisdemo.service.impl;
import com.redisdemo.mapper.StockMapper;
import com.redisdemo.model.Stock;
import com.redisdemo.service.IStockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
@Slf4j
@Service
public class IStockServiceImpl implements IStockService {
@Autowired
private StockMapper stockMapper;
@Override
public void decrByStock(String stockName) {
List<Stock> stockList = stockMapper.selectByExample(stockName);
if (!CollectionUtils.isEmpty(stockList)){
Stock stock = stockList.get(0);
stock.setStock(stock.getStock() - 1);
stockMapper.updateByPrimaryKey(stock);
}
}
@Override
public Integer selectByExample(String stockName) {
List<Stock> stockList = stockMapper.selectByExample(stockName);
if (!CollectionUtils.isEmpty(stockList)){
return stockList.get(0).getStock();
}
return 0;
}
}
package com.redisdemo.service;
import com.redisdemo.config.MyRabbitMQConfig;
import com.redisdemo.model.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class MQOrderService {
@Autowired
private IOrderService orderService;
/**
* 监听消息队列,并消费
* @param order 订单
*/
@RabbitListener(queues = MyRabbitMQConfig.ORDER_QUEUE)
public void createOrder(Order order){
log.info("收到订单消息,订单用户为{},商品名称名称为:{}",order.getOrderUser(),order.getOrderName());
//调用数据库orderService创建订单信息
orderService.createOrder(order);
}
}
MQStockService
package com.redisdemo.service;
import com.redisdemo.config.MyRabbitMQConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class MQStockService {
@Autowired
private IStockService iStockService;
@RabbitListener(queues = MyRabbitMQConfig.STORY_QUEUE)
public void decrByStock(String stockName){
log.info("库存消息队列收到的消息商品信息是:{}",stockName);
//调用数据库service给数据库对应商品库存减一
iStockService.decrByStock(stockName);
}
}
7、创建mapper层
OrderMapper
package com.redisdemo.mapper;
import com.redisdemo.model.Order;
public interface OrderMapper {
void insert(Order order);
}
StockMapper
package com.redisdemo.mapper;
import com.redisdemo.model.Stock;
import java.util.List;
public interface StockMapper {
/**
* 查询商品
* @param name 商品名
* @return
*/
List<Stock> selectByExample(String name);
/**
* 商品减库存-
* @param stock 商品对象
*/
void updateByPrimaryKey(Stock stock);
}
创建mybatis框架的xml映射文件
在resources目录下创建文件夹mapper
OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<!-- mapper 为根元素节点, 一个namespace对应一个dao -->
<mapper namespace="com.redisdemo.mapper.OrderMapper">
<insert id="insert" parameterType="com.redisdemo.model.Order">
INSERT INTO `db_concurrency`.`order`( `order_name`, `order_user`) VALUES ( #{orderName}, #{orderUser});
</insert>
</mapper>
StockMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<!-- mapper 为根元素节点, 一个namespace对应一个dao -->
<mapper namespace="com.redisdemo.mapper.StockMapper">
<!--查询商品名称-->
<select id="selectByExample" resultType="com.redisdemo.model.Stock">
SELECT
stock.id,
stock.`name`,
stock.stock
FROM
stock where name = #{name}
</select>
<!-- 商品减库存-->
<update id="updateByPrimaryKey" parameterType="com.redisdemo.model.Stock">
UPDATE `db_concurrency`.`stock` SET `stock` = #{stock} WHERE `id` = #{id};
</update>
</mapper>
8、controll层
package com.redisdemo.controller;
import com.redisdemo.config.MyRabbitMQConfig;
import com.redisdemo.model.Order;
import com.redisdemo.service.IOrderService;
import com.redisdemo.service.IRedisService;
import com.redisdemo.service.IStockService;
import com.redisdemo.service.MQStockService;
import com.sun.org.apache.xpath.internal.operations.Or;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisServer;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Controller
public class SecController {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private IRedisService redisService ;
@Autowired
private IOrderService orderService;
@Autowired
private MQStockService mqStockService;
@Autowired
private IStockService stockService;
/**
* 使用redis+消息队列进行秒杀实现
* @param username
* @param stockName
* @return
*/
@RequestMapping("/a")
@ResponseBody
public String sec(String username,String stockName){
log.info("参加秒杀的用户是:{},秒杀的商品是:{}",username,stockName);
String message = null;
Long decrBy = redisService.decrBy(stockName);
if (decrBy >= 0){
//说明该商品的库存量有剩余,可以进行下单操作
log.info("用户:{}秒杀该商品:{}库存有余,可以进行订单操作",username,stockName);
//发消息给订单消息队列,创建订单
Order order = new Order();
order.setOrderName(stockName);
order.setOrderUser(username);
rabbitTemplate.convertAndSend(MyRabbitMQConfig.ORDER_EXCHANGE,MyRabbitMQConfig.ORDER_ROUTING_KEY,order);
message = "用户" + username + "秒杀" + stockName + "成功";
}else {
//说明该商品的库存没有剩余,直接返回秒杀失败的消息给用户
log.info("用户:{}秒杀商品的库存存量没有剩余,秒杀结束",username);
message = username + "商品的存量没有剩余,秒杀结束";
}
return message;
}
@GetMapping("/b")
@ResponseBody
public String secDataBase(String username,String stockName){
log.info("参加秒杀的用户是:{},秒杀的商品是:{}",username,stockName);
String message = null;
//查找商品库存
Integer stockCount = stockService.selectByExample(stockName);
log.info("用户:{}参加秒杀,当前商品库存量是:{}",username,stockCount);
if (stockCount > 0){
//库存减一
stockService.decrByStock(stockName);
//下订单
Order order = new Order();
order.setOrderUser(username);
order.setOrderName(stockName);
orderService.createOrder(order);
log.info("用户:{}参加秒杀结果是:成功",username);
message = username + "参加秒杀结果是:成功";
}else {
log.info("用户:{}参加秒杀结果是:秒杀已经结束",username);
message = username + "参加秒杀活动结果是:秒杀已经结束";
}
return message;
}
}
9、在启动类添加方法
package com.redisdemo;
import com.redisdemo.service.IRedisService;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.annotation.MapperScans;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@Slf4j
@SpringBootApplication
@MapperScan("com.redisdemo.mapper")
public class RedisDemoApplication implements ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(RedisDemoApplication.class, args);
}
@Autowired
private IRedisService redisService;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("初始化数据库成功");
redisService.put("watch", 100, 20);
}
}
10、使用jmeter进行模拟多人同时秒杀
安装请看博客
推荐教程博客 :Jmeter压力测试工具安装及使用教程 作者roundlight
创建测试计划
点击启动,项目日志打印:
redis数据库数据:
jemeter 察看结果树:
至此,这里使用了redis+rabbitmq实现了高并发秒杀场景,并有效的防止了超卖现象,同时听过使用纯数据库操作产生了超卖的现象。