主要内容
- Redis预见库存减少数据库访问
- 内存标记减少Redis访问
- RabbitMQ队列缓冲,异步下单,增强用户体验
- RabbitMQ安装与Spring Boot集成
- 访问Nginx水平扩展
- 压测
思路:减少数据库访问
- 系统初始化,把商品库存数量加载到Redis中
- 收到请求,Redis预减库存,库存不足,直接返回,否则进入3
- 请求入队,立即返回排队中
- 请求出队,生成订单,减少库存
- 客户端轮询,是否秒杀成功
一、集成RabbitMQ
安装并启动RabbitMQ
- 安装erlang
- 安装RabbitMQ
- 启动RabbitMQ
./rabbitmq-server
netstat -nap|grep 5672
./rabbitmqctl stop
导包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置
#rabbitmq
spring.rabbitmq.host=192.168.174.10
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
#消费者数量
spring.rabbitmq.listener.simple.concurrency= 10
spring.rabbitmq.listener.simple.max-concurrency= 10
#从队列中每次取几个
spring.rabbitmq.listener.simple.prefetch= 1
#消费者自动启动
spring.rabbitmq.listener.simple.auto-startup=true
#消费者消费失败重新放回队列
spring.rabbitmq.listener.simple.default-requeue-rejected= true
#队列满了可以重试
spring.rabbitmq.template.retry.enabled=true
spring.rabbitmq.template.retry.initial-interval=1000
spring.rabbitmq.template.retry.max-attempts=3
spring.rabbitmq.template.retry.max-interval=10000
spring.rabbitmq.template.retry.multiplier=1.0
二、RabbitMQ的使用介绍
发送者先把数据发送到交换机上然后交换机在发送到对应的queue,由此引出四种模式
新建rabbitmq包
Direct模式的使用
1.MQConfig
@Configuration
public class MQConfig {
public static final String QUEUE = "queue";
/**
* Direct模式 交换机Exchange
* */
@Bean
public Queue queue() {
return new Queue(QUEUE, true);
}
}
2.MQSender
@Service
public class MQSender {
private static Logger log = LoggerFactory.getLogger(MQSender.class);
@Autowired
AmqpTemplate amqpTemplate ;
public void send(Object message) {
String msg = RedisService.beanToString(message);
log.info("send message:"+msg);
amqpTemplate.convertAndSend(MQConfig.QUEUE, msg);
}
}
3.MQReceiver
@Service
public class MQReceiver {
private static Logger log = LoggerFactory.getLogger(MQReceiver.class);
@RabbitListener(queues=MQConfig.QUEUE)
public void receive(String message) {
log.info("receive message:"+message);
}
}
4.写个controller测试一波
Topic模式的使用
交换机根据key把消息发送到对应绑定的queue
1.MQConfig
@Configuration
public class MQConfig {
public static final String TOPIC_QUEUE1 = "topic.queue1";
public static final String TOPIC_QUEUE2 = "topic.queue2";
public static final String TOPIC_EXCHANGE = "topicExchage";
/**
* Topic模式 交换机Exchange
* */
@Bean
public Queue topicQueue1() {
return new Queue(TOPIC_QUEUE1, true);
}
@Bean
public Queue topicQueue2() {
return new Queue(TOPIC_QUEUE2, true);
}
@Bean
public TopicExchange topicExchage(){
return new TopicExchange(TOPIC_EXCHANGE);
}
@Bean
public Binding topicBinding1() {
return BindingBuilder.bind(
topicQueue1())
.to(topicExchage()).
with("topic.key1");
}
@Bean
public Binding topicBinding2() {
//*代表一个单词,#代表多个单词
return BindingBuilder.bind(
topicQueue2())
.to(topicExchage()).
with("topic.#");
}
}
2.MQSender
@Service
public class MQSender {
private static Logger log = LoggerFactory.getLogger(MQSender.class);
@Autowired
AmqpTemplate amqpTemplate ;
public void sendTopic(Object message) {
String msg = RedisService.beanToString(message);
log.info("send topic message:"+msg);
//也就是交换机会根据key的值传递给绑定的queue,可以传递给多个queue只要匹配上
amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key1", msg+"1");
amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key2", msg+"2");
}
}
3.MQReceiver
@Service
public class MQReceiver {
private static Logger log = LoggerFactory.getLogger(MQReceiver.class);
@RabbitListener(queues=MQConfig.TOPIC_QUEUE1)
public void receiveTopic1(String message) {
log.info(" topic queue1 message:"+message);
}
@RabbitListener(queues=MQConfig.TOPIC_QUEUE2)
public void receiveTopic2(String message) {
log.info(" topic queue2 message:"+message);
}
}
4.写个controller测试一波
Fanout模式的使用
交换机把消息发送给所有队列
1.MQConfig
@Configuration
public class MQConfig {
public static final String TOPIC_QUEUE1 = "topic.queue1";
public static final String TOPIC_QUEUE2 = "topic.queue2";
public static final String FANOUT_EXCHANGE = "fanoutxchage";
/**
* Fanout模式 交换机Exchange
* */
@Bean
public Queue topicQueue1() {
return new Queue(TOPIC_QUEUE1, true);
}
@Bean
public Queue topicQueue2() {
return new Queue(TOPIC_QUEUE2, true);
}
@Bean
public FanoutExchange fanoutExchage(){
return new FanoutExchange(FANOUT_EXCHANGE);
}
@Bean
public Binding FanoutBinding1() {
return BindingBuilder.bind(topicQueue1()).to(fanoutExchage());
}
@Bean
public Binding FanoutBinding2() {
return BindingBuilder.bind(topicQueue2()).to(fanoutExchage());
}
}
2.MQSender
@Service
public class MQSender {
private static Logger log = LoggerFactory.getLogger(MQSender.class);
@Autowired
AmqpTemplate amqpTemplate ;
public void sendFanout(Object message) {
String msg = RedisService.beanToString(message);
log.info("send fanout message:"+msg);
//不用添加key,广播发送
amqpTemplate.convertAndSend(MQConfig.FANOUT_EXCHANGE, "", msg);
}
}
3.MQReceiver
@Service
public class MQReceiver {
private static Logger log = LoggerFactory.getLogger(MQReceiver.class);
@RabbitListener(queues=MQConfig.TOPIC_QUEUE1)
public void receiveTopic1(String message) {
log.info(" topic queue1 message:"+message);
}
@RabbitListener(queues=MQConfig.TOPIC_QUEUE2)
public void receiveTopic2(String message) {
log.info(" topic queue2 message:"+message);
}
}
4.写个controller测试一波
Header模式的使用
发送的是原始数据加头部信息,如果头部信息匹配了则发送到对应的queue
1.MQConfig
public class MQConfig {
public static final String HEADER_QUEUE = "header.queue";
public static final String HEADERS_EXCHANGE = "headersExchage";
/**
* Header模式 交换机Exchange
* */
@Bean
public HeadersExchange headersExchage(){
return new HeadersExchange(HEADERS_EXCHANGE);
}
@Bean
public Queue headerQueue1() {
return new Queue(HEADER_QUEUE, true);
}
@Bean
public Binding headerBinding() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("header1", "value1");
map.put("header2", "value2");
return BindingBuilder.bind(
headerQueue1()).
to(headersExchage()).
whereAll(map).match();
}
}
2.MQSender
@Service
public class MQSender {
private static Logger log = LoggerFactory.getLogger(MQSender.class);
@Autowired
AmqpTemplate amqpTemplate ;
public void sendHeader(Object message) {
String msg = RedisService.beanToString(message);
log.info("send header message:"+msg);
MessageProperties properties = new MessageProperties();
properties.setHeader("header1", "value1");
properties.setHeader("header2", "value2");
Message obj = new Message(msg.getBytes(), properties);
//发送的是原始数据加头部信息,如果头部信息匹配了则发送到对应的queue
amqpTemplate.convertAndSend(MQConfig.HEADERS_EXCHANGE, "", obj);
}
}
3.MQReceiver
@Service
public class MQReceiver {
private static Logger log = LoggerFactory.getLogger(MQReceiver.class);
@RabbitListener(queues=MQConfig.HEADER_QUEUE)
public void receiveHeaderQueue(byte[] message) {
log.info(" header queue message:"+new String(message));
}
}
4.写个controller测试一波
三、秒杀接口优化
分析以前的流程
优化思路
- 系统初始化,把商品库存数量加载到Redis
- 收到请求,Redis预见库存,库存不足,直接返回,否则进入3
- 请求入队,立即返回排队中(异步下单)
- 请求出队,生成订单,减少库存,把订单写入Redis中
- 客户端轮询,判断是否秒杀成功
代码实现
1.系统初始化时把库存加载到数据库
MiaoshaController 继承InitializingBean实现afterPropertiesSet方法即可
@Controller
@RequestMapping("/miaosha")
public class MiaoshaController implements InitializingBean {
。。。忽略其他代码
@Autowired
RedisService redisService;
@Autowired
GoodsService goodsService;
//系统初始化时把商品库存读取到Redis中
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVo> goodsList = goodsService.listGoodsVo();
if(goodsList == null) {
return;
}
for(GoodsVo goods : goodsList) {
redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
}
}
2.修改miaosha方法
@RequestMapping(value="/do_miaosha", method=RequestMethod.POST)
@ResponseBody
public Result<Integer> miaosha(Model model, MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
//redis预减库存
//预减库存
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10
if(stock < 0) {
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判断是否已经秒杀到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//入队(以前是减库存,下订单,写入秒杀订单)
MiaoshaMessage mm = new MiaoshaMessage();//见3
mm.setUser(user);
mm.setGoodsId(goodsId);
sender.sendMiaoshaMessage(mm);//见4
return Result.success(0);//排队中
}
}
3.MiaoshaMessage
public class MiaoshaMessage {
private MiaoshaUser user;
private long goodsId;
4.RabbitMQ相关
MQSender.sendMiaoshaMessage
@Service
public class MQSender {
private static Logger log = LoggerFactory.getLogger(MQSender.class);
@Autowired
AmqpTemplate amqpTemplate ;
public void sendMiaoshaMessage(MiaoshaMessage mm) {
String msg = RedisService.beanToString(mm);
log.info("send message:"+msg);
amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}
}
MQReceiver
主要完成写入秒杀订单
@Service
public class MQReceiver {
private static Logger log = LoggerFactory.getLogger(MQReceiver.class);
@Autowired
RedisService redisService;
@Autowired
GoodsService goodsService;
@Autowired
OrderService orderService;
@Autowired
MiaoshaService miaoshaService;
@RabbitListener(queues=MQConfig.MIAOSHA_QUEUE)
public void receive(String message) {
log.info("receive message:"+message);
MiaoshaMessage mm = RedisService.stringToBean(message, MiaoshaMessage.class);
MiaoshaUser user = mm.getUser();
long goodsId = mm.getGoodsId();
//判断数据库中的库存
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
if(stock <= 0) {
return;
}
//判断是否已经秒杀到了,其实因为表中加了唯一索引也可以不用这一步
//但是无论如何都会访问一次数据库--要么查询,要么更新
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
return;
}
//减库存 下订单 写入秒杀订单
miaoshaService.miaosha(user, goods);
}
}
MQConfig
@Configuration
public class MQConfig {
public static final String MIAOSHA_QUEUE = "miaosha.queue";
/**
* Direct模式 交换机Exchange
* */
@Bean
public Queue queue() {
return new Queue(MIAOSHA_QUEUE, true);
}
}
几个问题
1.下图中如果减库存失败了可以直接返回所以修改一下
解决
2.下图中insert返回的并不是订单id而是插入的数量
解决
5.处理页面
goods_details.htm页面
getMiaoshaResult
function getMiaoshaResult(goodsId){
g_showLoading();
$.ajax({
//见6
url:"/miaosha/result",
type:"GET",
data:{
goodsId:$("#goodsId").val(),
},
success:function(data){
if(data.code == 0){
var result = data.data;
if(result < 0){
layer.msg("对不起,秒杀失败");
}else if(result == 0){//继续轮询
setTimeout(function(){
getMiaoshaResult(goodsId);
}, 50);
}else{
layer.confirm("恭喜你,秒杀成功!查看订单?", {btn:["确定","取消"]},
function(){
window.location.href="/order_detail.htm?orderId="+result;
},
function(){
layer.closeAll();
});
}
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客户端请求有误");
}
});
}
6.处理逻辑
MiaoshaController
MiaoshaService
为了能够判断出是否卖完了还是RabbitMQ还没来得及处理,在Redis中做一个标记,而做标记的时间就是判断出已经卖完了的时候,所以如下图:
内存标记减少Redis访问
看红框里的,秒杀前预减库存,如果已经减到0了后续的判断时还是会访问Redis判断,我们可以想办法让Redis中库存已经为0时,不在访问Redis判断
解决方法
1.设置本地标记,并全部初始化为没有秒杀完,即false
2.在Redis预减库存前先从本地map中判断
四、Nginx横向扩展配置
找到nginx.conf文件,这里只是简单介绍。
反向代理
负载均衡
开启缓存
五、一般网站架构
或者再加一层LVS应付更大的并发