目前设想的大致的序列图
秒杀开始前,初始化数据库秒杀信息,并同步到redis缓存中,秒杀开始后,用户直接访问redis缓存进行库存扣减,当剩余库存小于0时说明商品抢购完毕,直接返回库存不足抢购失败,抢购成功的用户返回“秒杀成功,订单处理中,请稍后查看”,并且成功的抢购信息进入队列,异步扣减数据库实际库存并下单。用户查询订单,根据用户和商品查询对应的订单信息返回给用户。
1、减订单sql:
update product set stock = stock -1 where id = '' and stock > 0; -- 防止数据库层超卖
2、增加库存是否售完的内存map concurrentHashMap 用于存放是否售完的标识,分布式情况下,内存map不同步问题,考虑使用redis、zk(zookeeper)进行状态同步
准备
JMeter:用于模拟多线程用户秒杀
ActiveMQ:消息队列
redis:缓存
mysql:数据库
后台:idea开发,jdk8,springboot + mybatis + druid
环境搭建(前面已经介绍了springboot+mybatis+druid+activemq+redis的整合)
mysql创建表 tproduct-商品 torder-订单
启动redis和activemq
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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>test2</artifactId> <version>0.0.1-SNAPSHOT</version> <name>test2</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-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-activemq</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.properties
#tomcat 配置 默认8080 可以改成其他端口,这里显式配置 server.port=8080 #activemq 配置 用户名密码 用默认值 spring.activemq.broker-url=tcp://0.0.0.0:61616 #spring.jms.template.default-destination=test-queue spring.jms.template.default-destination=flash-queue #druid数据源 spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver spring.datasource.druid.url=jdbc:mysql://192.168.1.104:3306/test?useUnicode=true&characterEncoding=utf-8 spring.datasource.druid.username=root spring.datasource.druid.password=root #数据库连接池配置 spring.datasource.druid.initial-size=5 spring.datasource.druid.max-active=20 spring.datasource.druid.min-idle=5 spring.datasource.druid.max-wait=30000 #mybatis mybatis.mapper-locations=classpath:mapper/*.xml #mybatis.type-aliases-package=com.flysand.demo.entity #redis配置 # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=192.168.1.113 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password=test123 # 连接池最大连接数(使用负值表示没有限制) # spring boot 1版本配置 #spring.redis.pool.max-active=8 #spring boot 2 版本配置 spring.redis.jedis.pool.max-active=10 # 连接池最大阻塞等待时间(使用负值表示没有限制) #spring.redis.pool.max-wait=-1ms spring.redis.jedis.pool.max-wait=-1 # 连接池中的最大空闲连接 #spring.redis.pool.max-idle=8 spring.redis.jedis.pool.max-idle=8 # 连接池中的最小空闲连接 #spring.redis.pool.min-idle=0 spring.redis.jedis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=2000ms #默认logback日志配置 #日志文件配置 path为空,则在项目根目录 file为空,则默认为spring.log logging.path= logging.file=test2.log #日志级别 root级别 logging.level.root=info #自定义包日志级别 logging.level.com.flysand=debug #格式 - 控制台 logging.pattern.console=[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] - [%-5level][%logger{50}:%line] - %msg%n #文件 日期默认格式yyyy-MM-dd HH:mm:ss.SSS logging.pattern.file=[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] - [%-5level][%logger{50}:%line] - %msg%n
ProductServiceImpl.java
package com.flysand.demo.service.impl; import com.flysand.demo.activemq.ActiveMqProducer; import com.flysand.demo.dao.TProductMapper; import com.flysand.demo.entity.TProduct; import com.flysand.demo.service.ProductService; import com.flysand.demo.util.RedisUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author flysand on 2019/04/18 **/ @Service public class ProductServiceImpl implements ProductService { private static final Logger logger = LoggerFactory.getLogger(ProductServiceImpl.class); @Autowired private TProductMapper tProductMapper; @Autowired private RedisUtils redisUtils; @Autowired private ActiveMqProducer producer; @Override public int createProduct(TProduct product) { return tProductMapper.insert(product); } @Override public int decreaseProduct(String productId) { return tProductMapper.decreaseById(productId); } @Override public int getStockById(String productId) { return tProductMapper.selectProductStock(productId); } @Override public TProduct getProductById(String productId) { return tProductMapper.selectById(productId); } @Override public String syncStock(String productId) { String result = "同步库存成功"; try { int count = tProductMapper.selectProductStock(productId); redisUtils.setString(productId, String.valueOf(count)); } catch (Exception e) { logger.error("同步库存异常:{}", e.getMessage()); result = "同步库存异常"; } return result; } @Override public String flash(String key) { String result = "抢购提交成功,订单处理中"; // 原子减1 long count = (long) redisUtils.increase(key, -1L); String name = Thread.currentThread().getName(); // 把redis减1后还大于0即还有库存的设为抢购成功,并放入成功队列 if (count >= 0) { result += ",抢购线程" + name + ",抢购1个,剩余" + count; //redisUtils.sset("success", name); // 推送队列 String msg = key + "," + name; producer.sendMessage(msg); } else { result = "库存不足,抢购失败" + name; // redisUtils.sset("fail", name); } return result; } }
ActiveMqConsumer.java
package com.flysand.demo.activemq; import com.flysand.demo.entity.TOrder; import com.flysand.demo.entity.TProduct; import com.flysand.demo.service.OrderService; import com.flysand.demo.service.ProductService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jms.annotation.JmsListener; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.Date; import java.util.Random; /** * @author flysand on 2019/04/11 **/ @Component public class ActiveMqConsumer { private static final Logger logger = LoggerFactory.getLogger(ActiveMqConsumer.class); @Autowired private ProductService productService; @Autowired private OrderService orderService; @JmsListener(destination = "test-queue") public void receiveMessage(String text) { System.out.println("消费消息:" + text); } @JmsListener(destination = "flash-queue") public void invokeFlash(String text) { logger.debug("执行秒杀后的下单操作"); String productId = text.split(",")[0]; String threadName = text.split(",")[1]; // 查询当前商品库存 TProduct product = productService.getProductById(productId); if (product == null || product.getpCount() <= 0) { logger.error("商品不存在或库存不足"); } else { // 减少库存,并下单 productService.decreaseProduct(productId); TOrder order = new TOrder(); long time = System.currentTimeMillis(); order.setOrderNo("P_" + time); order.setProductId(productId); order.setQuantity(BigDecimal.ONE); order.setTotalAmount(product.getUnitPrice().multiply(BigDecimal.ONE)); order.setThreadName(threadName); orderService.createOrder(order); } } }
测试
由于本机资源有限,因此把项目打成jar包进行运行,测试通过jmeter模拟20W请求,秒杀1500商品 .
20W请求全部执行完,大概花费4分钟,消息队列异步下单大概花费5分钟,实际情况换成分布式的服务应该会快一些,但1500个订单全部下单成功,且没有失败订单。
后续增加多商品,以及下单信息回传队列,增加异常订单推送,以及websocket直接响应结果给前台。