前言
为什么要写秒杀系统呢,像现在限时秒杀已经成为电商项目中的一种常态,像现在的一些龙头电商企业都具备限时秒杀活动,这样能调动用户的购买兴趣的活动,能够给项目带来更大的活力,所以对于这个秒杀功能不说是一定能写,但是一定要了解他。
原理
秒杀与其他业务最大的区别在于,在秒杀的瞬间,系统的并发量和吞吐量会非常大,与此同时,网络的流量也会瞬间变大。
对于系统并发量变大问题,这里的核心在于如何在大并发的情况下保证数据库能扛得住压力,因为大并发的瓶颈在于数据库。如果用户的请求直接从前端传到数据库,显然,数据库是无法承受几十万上百万甚至上千万的并发量的。因此,我们能做的只能是减少对数据库的访问。例如,前端发出了100万个请求,通过我们的处理,最终只有10个会访问数据库,这样就会大大提升系统性能。再针对秒杀这种场景,因为秒杀商品的数量是有限的,因此这种做法刚好适用。
那么具体是如何来减少对数据库的访问的呢?
假如,某个商品可秒杀的数量是10,那么在秒杀活动开始之前,把商品的ID和数量加载到Redis缓存。当服务端收到请求时,首先预减Redis中的数量,如果数量减到小于0时,那么随后的访问直接返回秒杀失败的信息。也就是说,最终只有10个请求会去访问数据库。
如果商品数量比较多,比如1万件商品参与秒杀,那么就有1万*10=10万个请求并发去访问数据库,数据库的压力还是会很大。这里就用到了另外一个非常重要的组件:消息队列。我们不是把请求直接去访问数据库,而是先把请求写到消息队列中,做一个缓存,然后再去慢慢的更新数据库。这样做之后,前端用户的请求可能不会立即得到响应是成功还是失败,很可能得到的是一个排队中的返回值,这个时候,需要客户端去服务端轮询,因为我们不能保证一定就秒杀成功了。当服务端出队,生成订单以后,把用户ID和商品ID写到缓存中,来应对客户端的轮询就可以了。
这样处理以后,我们的应用是可以很简单的进行分布式横向扩展的,以应对更大的并发。
工具
本次使用的开发工具IDEA
、Redis
、RabittMQ
、MySql
关于安装Redis
工具请参考博客redis安装
关于安装RabittMQ
工具请参考博客win10安装rabbitmq
开发环境
使用的开发技术包括
SpringBoot 2.2.1
: 用来简化Spring应用的创建、运行、调试、部署 的框架MySQL 8.0.15
:关系型数据库Mybatis
:半自动化的对象关系映射的框架RabittMQ 3.7.14
:用于实现队列,想要了解实现延时队列可以参考我的上一篇博客Redis 3.0.5
:非关系型数据库,缓存,用于减少数据库压力,防止超卖
准备
-
数据库
sql
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for t_order -- ---------------------------- DROP TABLE IF EXISTS `t_order`; CREATE TABLE `t_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `order_user` int(255) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of t_order -- ---------------------------- INSERT INTO `t_order` VALUES (1, 'watch', 1); INSERT INTO `t_order` VALUES (2, 'watch', 1); INSERT INTO `t_order` VALUES (3, 'watch', 1); INSERT INTO `t_order` VALUES (4, 'watch', 10); INSERT INTO `t_order` VALUES (5, 'watch', 8); INSERT INTO `t_order` VALUES (6, 'watch', 93); INSERT INTO `t_order` VALUES (7, 'watch', 7); INSERT INTO `t_order` VALUES (8, 'watch', 42); INSERT INTO `t_order` VALUES (9, 'watch', 46); INSERT INTO `t_order` VALUES (10, 'watch', 86); INSERT INTO `t_order` VALUES (11, 'watch', 88); INSERT INTO `t_order` VALUES (12, 'watch', 91); -- ---------------------------- -- Table structure for stock -- ---------------------------- DROP TABLE IF EXISTS `stock`; CREATE TABLE `stock` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `stock` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of stock -- ---------------------------- INSERT INTO `stock` VALUES (1, 'watch', '0'); SET FOREIGN_KEY_CHECKS = 1;
-
SpringBoot
pom文件<!--jar依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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> <!-- bulid插件 编译mapper层下的xml --> <resources> <resource> <directory>src/main/java/</directory> <includes> <include>com/weisen/springbootspk/mapper/**/*.xml</include> </includes> </resource> </resources>
-
application.yml
server: port: 8080 #端口 spring: rabbitmq: #配置rabbitmq virtual-host: / host: localhost #ip username: guest #账号 password: weisen #密码 application: name: concurrency-project redis: #配置redis host: localhost port: 6379 #redis端口 jedis: pool: max-active: 1024 max-wait: -1s max-idle: 200 datasource: #mysql连接 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC username: root password: root #配置mybatis mybatis: mapper-locations: classpath:mapper/*.xml
核心
config配置
-
RabittMQ配置
MyRabbitMQConfig
package com.weisen.springbootspk.config; import org.springframework.amqp.core.*; 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; /** * 作者:weisen * 日期:2019/12/23 20:56 */ @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 MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } //创建库存交换机 @Bean public Exchange getStoryExchange() { return ExchangeBuilder.directExchange(STORY_EXCHANGE).durable(true).build(); } //创建库存队列 @Bean public Queue getStoryQueue() { return new Queue(STORY_QUEUE); } //库存交换机和库存队列绑定 @Bean public Binding bindStory() { return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs(); } //创建订单队列 @Bean public Queue getOrderQueue() { return new Queue(ORDER_QUEUE); } //创建订单交换机 @Bean public Exchange getOrderExchange() { return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build(); } //订单队列与订单交换机进行绑定 @Bean public Binding bindOrder() { return BindingBuilder.bind(getOrderQueue()).to(getOrderExchange()).with(ORDER_ROUTING_KEY).noargs(); } }
-
RedisConfig配置
RedisConfig
package com.weisen.springbootspk.config; 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; /** * 作者:weisen * 日期:2019/12/23 20:57 */ @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); template.setConnectionFactory(redisConnectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; } }
pojo 实体
实体中未添加get set方法是因为使用了lombok插件
idea如何使用lombok可参考idea中springboot项目使用lombok 插件
-
Order
package com.weisen.springbootspk.pojo; import lombok.Data; import java.io.Serializable; /** * 作者:weisen * 日期:2019/12/23 20:45 */ @Data public class Order implements Serializable { private static final long serialVersionUID = -8271355836132430489L; Integer id; String orderName; String orderUser; }
-
Stock
package com.weisen.springbootspk.pojo; import lombok.Data; import org.springframework.data.annotation.Id; import java.io.Serializable; /** * 作者:weisen * 日期:2019/12/24 9:11 */ @Data public class Stock implements Serializable { private static final long serialVersionUID = 6235666939721331057L; Integer id; String name; Integer stock; }
mapper层
-
order
package com.weisen.springbootspk.mapper.order; import com.weisen.springbootspk.pojo.Order; import org.apache.ibatis.annotations.Mapper; /** * 作者:weisen * 日期:2019/12/24 9:32 */ @Mapper public interface OrderMapper { Integer insert(Order order); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="Order mapper接口的相对路径"> <insert id="insert" parameterType="Order实体的相对路径"> insert t_order(order_name,order_user) value (#{orderName},#{orderUser}) </insert> </mapper>
-
stock
package com.weisen.springbootspk.mapper.stock; import com.weisen.springbootspk.pojo.Stock; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; /** * 作者:weisen * 日期:2019/12/24 9:30 */ @Mapper public interface StockMapper { List<Stock> selectList(@Param("name") String name); Integer updateByPrimaryKey(Stock stock); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="stock mapper接口的相对路径"> <select id="selectList" resultType="Stock实体的相对路径"> select * from stock where name = #{name} </select> <update id="updateByPrimaryKey" parameterType="Stock实体的相对路径"> update stock set stock = #{stock} where id = #{id}; </update> </mapper>
service层
-
订单跟库存的消息和消费的请求
MQOrderService
监听订单消息队列,并消费package com.weisen.springbootspk.service; import com.weisen.springbootspk.config.MyRabbitMQConfig; import com.weisen.springbootspk.pojo.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; /** * 作者:weisen * 日期:2019/12/23 21:07 */ @Service @Slf4j public class MQOrderService { @Autowired private OrderService 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.weisen.springbootspk.service; import com.weisen.springbootspk.config.MyRabbitMQConfig; import lombok.extern.slf4j.Slf4j; import org.apache.logging.log4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * 作者:weisen * 日期:2019/12/23 21:05 */ @Service @Slf4j public class MQStockService { @Autowired private StockService stockService; /** * 监听库存消息队列,并消费 * @param stockName */ @RabbitListener(queues = MyRabbitMQConfig.STORY_QUEUE) public void decrByStock(String stockName) { log.info("库存消息队列收到的消息商品信息是:{}", stockName); /** * 调用数据库service给数据库对应商品库存减一 */ stockService.decrByStock(stockName); } }
-
Redis的处理
RedisService
package com.weisen.springbootspk.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.Date; import java.util.concurrent.TimeUnit; /** * 作者:weisen * 日期:2019/12/23 20:58 */ @Service public class RedisService { @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 设置String键值对 * @param key * @param value * @param millis */ public void put(String key, Object value, long millis) { redisTemplate.opsForValue().set(key, value, millis, TimeUnit.MINUTES); } public void putForHash(String objectKey, String hkey, String value) { redisTemplate.opsForHash().put(objectKey, hkey, value); } /** * 对指定key的键值减一 * @param key * @return */ public Long decrBy(String key) { return redisTemplate.opsForValue().decrement(key); } }
-
普通业务的调用(由于是一个简单的逻辑实现,就没有严格按照面向接口的方式实现)
OrderService
package com.weisen.springbootspk.service; import com.weisen.springbootspk.mapper.order.OrderMapper; import com.weisen.springbootspk.pojo.Order; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * 作者:weisen * 日期:2019/12/23 21:10 */ @Service public class OrderService{ @Autowired private OrderMapper orderMapper; public void createOrder(Order order) { orderMapper.insert(order); } }
StockService
package com.weisen.springbootspk.service; import com.weisen.springbootspk.mapper.stock.StockMapper; import com.weisen.springbootspk.pojo.Stock; 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; /** * 作者:weisen * 日期:2019/12/23 21:08 */ @Service @Slf4j public class StockService { @Autowired private StockMapper stockMapper; public void decrByStock(String stockName) { List<Stock> stocks = stockMapper.selectList(stockName); if (!CollectionUtils.isEmpty(stocks)) { Stock stock = stocks.get(0); stock.setStock(stock.getStock() - 1); stockMapper.updateByPrimaryKey(stock); } } public Integer selectByName(String stockName) { List<Stock> stocks = stockMapper.selectList(stockName); if (!CollectionUtils.isEmpty(stocks)) { return stocks.get(0).getStock().intValue(); } return 0; } }
controller
SecController
写了两种实现方式,可以看一看其中的区别的什么
package com.weisen.springbootspk.controller;
import com.weisen.springbootspk.config.MyRabbitMQConfig;
import com.weisen.springbootspk.pojo.Order;
import com.weisen.springbootspk.service.OrderService;
import com.weisen.springbootspk.service.RedisService;
import com.weisen.springbootspk.service.StockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 作者:weisen
* 日期:2019/12/23 21:03
*/
@Controller
@Slf4j
public class SecController {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisService redisService;
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
/**
* 使用redis+消息队列进行秒杀实现
* @param username
* @param stockName
* @return
*/
@RequestMapping("/sec")
@ResponseBody
public String sec(@RequestParam(value = "username") String username, @RequestParam(value = "stockName") String stockName) {
log.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName);
String message = null;
//调用redis给相应商品库存量减一
Long decrByResult = redisService.decrBy(stockName);
if (decrByResult >= 0) {
/**
* 说明该商品的库存量有剩余,可以进行下订单操作
*/
log.info("用户:{}秒杀该商品:{}库存有余,可以进行下订单操作", username, stockName);
//发消息给库存消息队列,将库存数据减一
rabbitTemplate.convertAndSend(MyRabbitMQConfig.STORY_EXCHANGE, MyRabbitMQConfig.STORY_ROUTING_KEY, 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;
}
/**
* 实现纯数据库操作实现秒杀操作
* @param username
* @param stockName
* @return
*/
@RequestMapping("/secDataBase")
@ResponseBody
public String secDataBase(@RequestParam(value = "username") String username, @RequestParam(value = "stockName") String stockName) {
log.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName);
String message = null;
//查找该商品库存
Integer stockCount = stockService.selectByExample(stockName);
log.info("用户:{}参加秒杀,当前商品库存量是:{}", username, stockCount);
if (stockCount > 0) {
/**
* 还有库存,可以进行继续秒杀,库存减一,下订单
*/
//1、库存减一
stockService.decrByStock(stockName);
//2、下订单
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;
}
}
SpringBoot启动类
@SpringBootApplication
public class SpringbootspkApplication implements ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(SpringbootspkApplication.class, args);
}
@Autowired
private RedisService redisService;
/**
* redis初始化各商品的库存量
* @param args
* @throws Exception
*/
@Override
public void run(ApplicationArguments args) throws Exception {
redisService.put("watch", 10, 20);
}
}
测试
使用工具 apache-jmeter-5.2.1
如何使用工具 可参考Apache JMeter5.2基础入门实践详解
-
启动
apache-jmeter-5.2.1
-
设置语言
-
创建线程组
-
在线程组上右键添加请求和监听器元件
-
编辑线程组
jmeter可以定义随机参数
点击绿色箭头运行