电商下单秒杀场景配套技术:MySql(商品库存和订单存储)+Redis(缓存商品库存,下单从redis减库存)+ActiveMQ(下单后同步减Mysql库存)

3 篇文章 0 订阅
2 篇文章 0 订阅

配套技术:MySql(商品库存和订单存储)+Redis(缓存商品库存,下单从redis减库存)+ActiveMQ(下单后同步减Mysql库存)

整体思路是:

1、设定秒杀库存,在商品上架/设定库存等接口,将库存存储到MySQL并在缓存Redis里也存储一份

2、模拟N个并发量和M个总访问量下单秒杀,这里下单不是真实下单,只是减掉Redis库存,然后想MQ发送下达请求

3、MQ监听器下单,这里是真实的生成订单记录,并从redis同步库存到MySql,多并发的情况在Redis做了排队减库存,并在MQ做了排队,避免了抢占库存情况。

源码展示:

redis和ActiveMQ依赖包

        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-pool</artifactId>
            <version>5.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-all</artifactId>
            <version>5.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <!-- for Spring Data Redis v.2.0.x -->
            <artifactId>redisson-spring-data-20</artifactId>
            <version>3.10.3</version>
        </dependency>

1、Redis配置

################### redis 单机版 start ##########################
    redis:
    host: ${ipconfig.redis}
    port: 6379
    timeout: 6000
    database8: 8
    lettuce:
      pool:
        max-active: 10 # 连接池最大连接数(使用负值表示没有限制),如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)
        max-idle: 8   # 连接池中的最大空闲连接 ,默认值也是8
        max-wait: 100 # # 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException
        min-idle: 2    # 连接池中的最小空闲连接 ,默认值也是0
      shutdown-timeout: 100ms
################### redis 单机版 end ##########################
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.redisson.spring.data.connection.RedissonConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;

@Configuration
public class Redis8Config {
	@Value("${spring.redis.host}")
	private String hostName;
	@Value("${spring.redis.port}")
	private int port;
	@Value("${spring.redis.lettuce.pool.max-idle}")
	private int maxIdle;
	@Value("${spring.redis.lettuce.pool.max-active}")
	private int maxTotal;
	@Value("${spring.redis.database8}")
	private int index8;
	@Value("${spring.redis.lettuce.pool.max-wait}")
	private int maxWaitMillis;

	@Bean(name = "redis8")
	public StringRedisTemplate stringRedisTemplate8( @Qualifier("redisson8") RedissonClient redisson8){
		StringRedisTemplate temple = new StringRedisTemplate();
		boolean testOnBorrow = false;
		temple.setConnectionFactory(new RedissonConnectionFactory(redisson8));
		return temple;
	}

	@Bean(destroyMethod = "shutdown")
	public RedissonClient redisson8() {
		RedissonClient redisson = Redisson.create(configRedisson(index8));
		return redisson;
	}

	private Config configRedisson(int database) {
		Config config = new Config();
		SingleServerConfig singleServerConfig = config.useSingleServer();
		singleServerConfig.setAddress(String.format("redis://%s:%d",hostName, port));
		singleServerConfig.setConnectionMinimumIdleSize(maxIdle);
		singleServerConfig.setConnectionPoolSize(maxTotal);
		if(maxWaitMillis > 0) {
			singleServerConfig.setIdleConnectionTimeout(maxWaitMillis);
		}
		singleServerConfig.setDatabase(database);
		return config;
	}

 2、接口控制层

import com.open.capacity.commons.Result;
import com.open.capacity.xinyidai.activemq.MQListerner;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.jms.*;

/**
 * 下单秒杀
 * @author:whp
 */
@Slf4j
@RestController
public class OrderController {
    @Autowired
    @Resource(name = "redis8")
    StringRedisTemplate redisTemplate;

    @Autowired
    MQListerner listener;
    
    /**
    * 这里可以是商品上架/设定库存等接口,库存存储到MySQL并在缓存Redis里也存储一份
    **/
    @ApiOperation(value = "设定秒杀库存")
    @RequestMapping(value = "stocks",method = RequestMethod.POST)
    public Result setSock() {
        redisTemplate.opsForValue().set("stock","100");
        return Result.succeed("SUCCESS");
    }

   /**
    * 这里下单不是真实下单,只是减掉Redis库存,然后想MQ发送下达请求
    **/
    @ApiOperation(value = "模拟N个并发量和M个总访问量下单秒杀")
    @RequestMapping(value = "orders",method = RequestMethod.POST)
    public Result orders() {
        new MultiThreadOrdersTest().run();
        System.out.println("finished!");
        return Result.succeed("SUCCESS");
    }

    /**
    * 这里是真实的生成订单记录,并从redis同步库存到MySql,多并发的情况在Redis做了排队减库存,并 
    * 在MQ做了排队,避免了抢占库存情况。
    **/
    @ApiOperation(value = "MQ监听器下单,并同步库存到Mysql")
    @RequestMapping(value = "consumer",method = RequestMethod.POST)
    public Result consumer() {
        // 连接工厂
        ConnectionFactory connectionFactory;
        // 连接实例
        Connection connection = null;
        // 收发的线程实例
        Session session;
        // 消息发送目标地址
        Destination destination;
        try {
            // 实例化连接工厂
            connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.10.218:61616");
            // 获取连接实例
            connection = connectionFactory.createConnection();
            // 启动连接
            connection.start();
            // 创建接收或发送的线程实例(消费者就不需要开启事务了)
            session = connection.createSession(Boolean.FALSE,Session.AUTO_ACKNOWLEDGE);
            // 创建队列(返回一个消息目的地)
            destination = session.createQueue("order-queue");
            // 创建消息消费者
            MessageConsumer consumer = session.createConsumer(destination);
            //注册消息监听
            consumer.setMessageListener(listener);
        } catch (JMSException e) {
            e.printStackTrace();
        }
        return Result.succeed("SUCCESS");
    }
}

3、模拟clientNum个客户端访问,建立ExecutorService线程池,threadNum个线程可以同时访问下单库存

import com.open.capacity.utils.SpringUtils;
import java.text.NumberFormat;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MultiThreadOrdersTest {
    public static StockService stockService = SpringUtils.getBean(StockService.class);
    static int count = 0;
    // 总访问量是clientNum,并发量是threadNum
    int threadNum = 4;
    int clientNum = 200;

    float avgExecTime = 0;
    float sumexecTime = 0;
    long firstExecTime = Long.MAX_VALUE;
    long lastDoneTime = Long.MIN_VALUE;
    float totalExecTime = 0;

    public void run() {

        final ConcurrentHashMap<Integer, ThreadRecord> records = new ConcurrentHashMap<Integer, ThreadRecord>();

        // 建立ExecutorService线程池,threadNum个线程可以同时访问
        ExecutorService exec = Executors.newFixedThreadPool(threadNum);
        // 模拟clientNum个客户端访问
        final CountDownLatch doneSignal = new CountDownLatch(clientNum);

        for (int i = 0; i < clientNum; i++) {
            Runnable run = new Runnable() {
                public void run() {
                    int index = getIndex();
                    long systemCurrentTimeMillis = System.currentTimeMillis();
                    try {
                        long stock = (long)(Math.random()*10)+1;
                        stockService.decreaseStock(Thread.currentThread().getName(),stock);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                    records.put(index, new ThreadRecord(systemCurrentTimeMillis, System.currentTimeMillis()));
                    doneSignal.countDown();// 每调用一次countDown()方法,计数器减1
                }
            };
            exec.execute(run);
        }

        try {
            // 计数器大于0 时,await()方法会阻塞程序继续执行
            doneSignal.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        /**
         * 获取每个线程的开始时间和结束时间
         */
        for (int i : records.keySet()) {
            ThreadRecord r = records.get(i);
            sumexecTime += ((double) (r.endTime - r.startTime)) / 1000;

            if (r.startTime < firstExecTime) {
                firstExecTime = r.startTime;
            }
            if (r.endTime > lastDoneTime) {
                this.lastDoneTime = r.endTime;
            }
        }

        this.avgExecTime = this.sumexecTime / records.size();
        this.totalExecTime = ((float) (this.lastDoneTime - this.firstExecTime)) / 1000;
        NumberFormat nf = NumberFormat.getNumberInstance();
        nf.setMaximumFractionDigits(4);

        System.out.println("======================================================");
        System.out.println("线程数量:\t\t" + threadNum);
        System.out.println("客户端数量:\t" + clientNum);
        System.out.println("平均执行时间:\t" + nf.format(this.avgExecTime) + "秒");
        System.out.println("总执行时间:\t" + nf.format(this.totalExecTime) + "秒");
        System.out.println("吞吐量:\t\t" + nf.format(this.clientNum / this.totalExecTime) + "次每秒");
    }

    public static int getIndex() {
        return ++count;
    }

}

class ThreadRecord {
    long startTime;
    long endTime;

    public ThreadRecord(long st, long et) {
        this.startTime = st;
        this.endTime = et;
    }
}

4、库存服务层Service,从redis减库存并像MQ发送下达消息

import com.alibaba.fastjson.JSON;
import com.open.capacity.xinyidai.activemq.MessageVo;
import com.open.capacity.xinyidai.activemq.QueueSender;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class StockService {

    @Autowired
    @Resource(name = "redis8")
    StringRedisTemplate redisTemplate;
    @Autowired
    @Resource(name = "redisson8")
    private RedissonClient redisson;

    /**
     * 从缓存减库存
     *
     * @return
     */
    public boolean decreaseStock(String clientName,long stock) {
        System.out.println("当前用户:" + clientName);
        RLock lock = redisson.getLock(clientName);
        lock.lock();
        boolean flag = false;
        try {
            RAtomicLong atomicStock = redisson.getAtomicLong("stock");
            if (!atomicStock.isExists()) {
                redisTemplate.opsForValue().set("stock", "100");
            }

            long currStock = atomicStock.get();
            if (currStock >= stock) {
                System.out.println("当前用户:" + clientName  + ",减库存数量:" + stock  +",减库存之后剩余库存:" + atomicStock.addAndGet(0-stock));
                QueueSender queueSender = new QueueSender();
                queueSender.sendMsg(JSON.toJSONString(new MessageVo(clientName,stock)));
                flag = true;
            } else {
                System.out.println("库存不足");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return flag;
    }
}

5、ActiveMQ发消息工具类

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import org.apache.activemq.ActiveMQConnectionFactory;

public class QueueSender {
    private static ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.10.218:61616");
    public static void sendMsg(String message) {
        Connection connection = null;
        Session session = null;
        try {
            connection = connectionFactory.createConnection();
            connection.start();
            session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);
            Destination destination = session.createQueue("order-queue");
            MessageProducer producer = session.createProducer(destination);
            producer.send(session.createTextMessage(message));
            session.commit();
        } catch (JMSException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != session) {
                    session.close();
                }
                if (null != connection) {
                    connection.close();
                }
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
    }
}

6、ActiveMQ消息消费者监听器

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
/**
 * <p>
 * MQListerner 生产者监听器
 * <p>
 */
@Service
public class MQListerner implements MessageListener{

    @Autowired
    private OrderDao orderDao;

    @Override
    public void onMessage(Message message) {
        try {
            String msgText = ((TextMessage)message).getText();
            MessageVo messageVo = JSON.parseObject(msgText,MessageVo.class);
            orderDao.save(messageVo);
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }
}

7、客户端实体对象

import lombok.Builder;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

@Builder
@Data
public class MessageVo implements Serializable {
    private Long id;
    private String client;
    private long stock;
    private Date createTime;

    public MessageVo(){}
    public MessageVo(String client,long stock){
        this.client = client;
        this.stock = stock;
    }
    public MessageVo(Long id,String client,long stock,Date createTime){
        this.id = id;
        this.client = client;
        this.stock = stock;
        this.createTime = createTime;
    }
}

8、PostMan调用接口地址测试

设定商品秒杀库存:http://192.168.10.218:56705/stocks 

启动下单消息监听:http://192.168.10.218:56705/consumer

模拟多客户多并发下单:http://192.168.10.218:56705/orders

 

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值