目前设想的大致的序列图

秒杀开始前,初始化数据库秒杀信息,并同步到redis缓存中,秒杀开始后,用户直接访问redis缓存进行库存扣减,当剩余库存小于0时说明商品抢购完毕,直接返回库存不足抢购失败,抢购成功的用户返回“秒杀成功,订单处理中,请稍后查看”,并且成功的抢购信息进入队列,异步扣减数据库实际库存并下单。用户查询订单,根据用户和商品查询对应的订单信息返回给用户。

SpringBoot+redis+activemq秒杀场景简单整理

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直接响应结果给前台。

 

git 源码:https://github.com/symflysand/secendsKill