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


    目前设想的大致的序列图

    秒杀开始前,初始化数据库秒杀信息,并同步到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直接响应结果给前台。

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

  • 相关阅读:
    git删除目录,且保留本地的
    gitpush 免密码
    git常用操作
    ubuntu安装Nodejs
    ubuntu如何配置samba
    用AI将png转成svg做字符图标教程
    windows server 2012设置远程连接断开后自动注销
    windows 2012执行计划任务错误:操作员或系统管理员拒绝了请求(0x800710E0)
    删除节点
    代理 XP”组件已作为此服务器安全配置的一部分被关闭。系统管理员可以使用 sp_configure 来启用“代理 XP”。
  • 原文地址:https://www.cnblogs.com/flysand/p/10737091.html
Copyright © 2020-2023  润新知