配套技术: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