springboot +rabbitmq+redis实现秒杀示例
作者:yy1209357299 发布时间:2022-04-21 02:29:28
实现说明
这里的核心在于如何在大并发的情况下保证数据库能扛得住压力,因为大并发的瓶颈在于数据库。如果用户的请求直接从前端传到数据库,显然,数据库是无法承受几十万上百万甚至上千万的并发量的。因此,我们能做的只能是减少对数据库的访问。例如,前端发出了100万个请求,通过我们的处理,最终只有10个会访问数据库,这样就会大大提升系统性能。再针对秒杀这种场景,因为秒杀商品的数量是有限的,因此采用上述实现方案。
假如,某个商品可秒杀的数量是10,那么在秒杀活动开始之前,把商品的ID和数量加载到Redis缓存。当服务端收到请求时,首先预减Redis中的数量,如果数量减到小于0时,那么随后的访问直接返回秒杀失败的信息。也就是说,最终只有10个请求会去访问数据库。
如果商品数量比较多,比如1万件商品参与秒杀,那么就有1万*10=10万个请求并发去访问数据库,数据库的压力还是会很大。这里就用到了另外一个非常重要的组件:消息队列。我们不是把请求直接去访问数据库,而是先把请求写到消息队列中,做一个缓存,然后再去慢慢的更新数据库。这样做之后,前端用户的请求可能不会立即得到响应是成功还是失败,很可能得到的是一个排队中的返回值,这个时候,需要客户端去服务端轮询,因为我们不能保证一定就秒杀成功了。当服务端出队,生成订单以后,把用户ID和商品ID写到缓存中,来应对客户端的轮询就可以了。这样处理以后,我们的应用是可以很简单的进行分布式横向扩展的,以应对更大的并发。当然,秒杀系统还有很多要处理的事情,比如限流防刷、分布式Session等等。
1、工具准备
rabbitmq安装:https://www.jb51.net/article/253706.htm
界面地址:http://localhost:15672/#/
用户名 guest
密码 guest
redis安装:https://www.jb51.net/article/145704.htm
jmeter安装:https://www.jb51.net/article/232152.htm
2、数据表
商品表
-- ----------------------------
-- Table structure for stock
-- ----------------------------
DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
`count` int(11) NOT NULL COMMENT '库存',
`sale` int(11) NOT NULL COMMENT '已售',
`version` int(11) NOT NULL COMMENT '乐观锁,版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
订单表
-- ----------------------------
-- Table structure for stock_order
-- ----------------------------
DROP TABLE IF EXISTS `stock_order`;
CREATE TABLE `stock_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`sid` int(11) NOT NULL COMMENT '库存ID',
`name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3、pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
4、代码结构
5、配置config
mq配置
package com.yy.msserver.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author code
* @Date 2022/6/27 14:03
* Description rabbitmq config
* Version 1.0
*/
@Configuration
public class MyRabbitMQConfig {
//库存交换机
public static final String STORY_EXCHANGE = "STORY_EXCHANGE";
//订单交换机
public static final String ORDER_EXCHANGE = "ORDER_EXCHANGE";
//库存队列
public static final String STORY_QUEUE = "STORY_QUEUE";
//订单队列
public static final String ORDER_QUEUE = "ORDER_QUEUE";
//库存路由键
public static final String STORY_ROUTING_KEY = "STORY_ROUTING_KEY";
//订单路由键
public static final String ORDER_ROUTING_KEY = "ORDER_ROUTING_KEY";
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
//创建库存交换机
@Bean
public Exchange getStoryExchange() {
return ExchangeBuilder.directExchange(STORY_EXCHANGE).durable(true).build();
}
//创建库存队列
@Bean
public Queue getStoryQueue() {
return new Queue(STORY_QUEUE);
}
//库存交换机和库存队列绑定
@Bean
public Binding bindStory() {
return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs();
}
//创建订单队列
@Bean
public Queue getOrderQueue() {
return new Queue(ORDER_QUEUE);
}
//创建订单交换机
@Bean
public Exchange getOrderExchange() {
return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
}
//订单队列与订单交换机进行绑定
@Bean
public Binding bindOrder() {
return BindingBuilder.bind(getOrderQueue()).to(getOrderExchange()).with(ORDER_ROUTING_KEY).noargs();
}
}
redis配置
package com.yy.msserver.config;
/**
* @author code
* @Date 2022/6/27 14:06
* Description redis config
* Version 1.0
*/
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author code
*@Date 2022/6/27 14:05
*Description redis config
*Version 1.0
*/
@Configuration
public class RedisConfig {
// 配置redis得配置详解
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
6、订单业务层
接口层
package com.yy.msserver.service;
import com.yy.msserver.model.vo.Stock;
/**
* @author code
* @Date 2022/6/24 9:25
* Description 订单接口
* Version 1.0
*/
public interface StockOrderService {
public Integer createOrder(Integer id);
public void decrByStock(Integer id);
}
实现层
package com.yy.msserver.service.impl;
import com.yy.msserver.dao.StockMapper;
import com.yy.msserver.dao.StockOrderMapper;
import com.yy.msserver.model.vo.Stock;
import com.yy.msserver.model.vo.StockOrder;
import com.yy.msserver.service.StockOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
* @author code
* @Date 2022/6/24 9:25
* Description 订单实现
* Version 1.0
*/
@Service
public class StockOrderServiceImpl implements StockOrderService {
@Autowired
private StockOrderMapper stockOrderMapper;
@Autowired
private StockMapper stockMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Integer createOrder(Integer id) {
//校验库存
Stock stock = checkStock(id);
// if(stock.getCount()>0){
// System.out.println("当前库存:" + stock.getCount());
// //扣库存
// if(updateSale(stock) == 1){
//
// }else {
// return 0;
// }
// }
// return 0;
return createOrder(stock);
}
@Override
public void decrByStock(Integer id){
//校验库存
Stock stock = checkStock(id);
if(stock.getCount()>0){
System.out.println("当前库存:" + stock.getCount());
//扣库存
updateSale(stock);
}
}
//校验库存
private Stock checkStock(Integer id) {
return stockMapper.checkStock(id);
}
//扣库存
private int updateSale(Stock stock){
return stockMapper.updateSale(stock);
}
//下订单
private Integer createOrder(Stock stock){
StockOrder order = new StockOrder();
order.setSid(stock.getId());
order.setCreateTime(new Date());
order.setName(stock.getName());
stockOrderMapper.createOrder(order);
return order.getId();
}
}
7、redis实现层
package com.yy.msserver.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @author code
* @Date 2022/6/27 17:25
* Description redis
* Version 1.0
*/
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 设置String键值对
* @param key
* @param value
* @param millis
*/
public void put(String key, Object value, long millis) {
redisTemplate.opsForValue().set(key, value, millis, TimeUnit.MINUTES);
}
public void putForHash(String objectKey, String hkey, String value) {
redisTemplate.opsForHash().put(objectKey, hkey, value);
}
public <T> T get(String key, Class<T> type) {
return (T) redisTemplate.boundValueOps(key).get();
}
public void remove(String key) {
redisTemplate.delete(key);
}
public boolean expire(String key, long millis) {
return redisTemplate.expire(key, millis, TimeUnit.MILLISECONDS);
}
public boolean persist(String key) {
return redisTemplate.hasKey(key);
}
public String getString(String key) {
return (String) redisTemplate.opsForValue().get(key);
}
public Integer getInteger(String key) {
return (Integer) redisTemplate.opsForValue().get(key);
}
public Long getLong(String key) {
return (Long) redisTemplate.opsForValue().get(key);
}
public Date getDate(String key) {
return (Date) redisTemplate.opsForValue().get(key);
}
/**
* 对指定key的键值减一
* @param key
* @return
*/
public Long decrBy(String key) {
return redisTemplate.opsForValue().decrement(key);
}
}
8、mq实现层
减库存
package com.yy.msserver.service;
import com.yy.msserver.config.MyRabbitMQConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author code
* @Date 2022/6/27 15:22
* Description mq商品信息
* Version 1.0
*/
@Slf4j
@Service
public class MQStockService {
@Autowired
private StockOrderService stockService;
/**
* 监听库存消息队列,并消费
* @param id
*/
@RabbitListener(queues = MyRabbitMQConfig.STORY_QUEUE)
public void decrByStock(Integer id) {
/**
* 调用数据库service给数据库对应商品库存减一
*/
log.info("减库存");
stockService.decrByStock(id);
}
}
下订单
package com.yy.msserver.service;
import com.yy.msserver.config.MyRabbitMQConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author code
* @Date 2022/6/27 15:19
* Description mq 订单队列
* Version 1.0
*/
@Service
@Slf4j
public class MQOrderService {
@Autowired
private StockOrderService orderService;
/**
* 监听订单消息队列,并消费
*
* @param id
*/
@RabbitListener(queues = MyRabbitMQConfig.ORDER_QUEUE)
public void createOrder(Integer id) {
log.info("收到订单消息");
orderService.createOrder(id);
}
}
9、redis模拟初始化库存量
package com.yy.msserver.config;
import com.yy.msserver.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @author code
* @Date 2022/6/29 14:08
* Description 初始化
* Version 1.0
*/
@Component
public class InitConfig {
@Autowired
private RedisService redisService;
/**
* redis初始化商品的库存量和信息
* @param
* @throws Exception
*/
@PostConstruct
public void init() {
redisService.put("1", 10, 20);
}
}
10、controller控制层
/**
* 使用redis+消息队列进行秒杀实现
*
* @param id 商品id
* @return
*/
@GetMapping( value = "/sec",produces = "application/json;charset=utf-8")
@ResponseBody
public String sec(@RequestParam(value = "id") int id) {
String message = null;
//调用redis给相应商品库存量减一
Long decrByResult = redisService.decrBy(id+"");
if (decrByResult >= 0) {
/**
* 说明该商品的库存量有剩余,可以进行下订单操作
*/
//发消息给库存消息队列,将库存数据减一
rabbitTemplate.convertAndSend(MyRabbitMQConfig.STORY_EXCHANGE, MyRabbitMQConfig.STORY_ROUTING_KEY, id);
//发消息给订单消息队列,创建订单
rabbitTemplate.convertAndSend(MyRabbitMQConfig.ORDER_EXCHANGE, MyRabbitMQConfig.ORDER_ROUTING_KEY, id);
message = "商品" + id + "秒杀成功";
} else {
/**
* 说明该商品的库存量没有剩余,直接返回秒杀失败的消息给用户
*/
message ="商品" + id + "秒杀商品的库存量没有剩余,秒杀结束";
}
return message;
}
11、测试
新建线程组——新建取样器(http请求)——新建查看结果树
12、测试结果
来源:https://blog.csdn.net/yy1209357299/article/details/125523406


猜你喜欢
- 队列是其元素按照先进先出(FIFO)的方式来处理的集合。队列使用System.Collections.Generic名称空间中的泛型类Que
- 前言Guava是google公司开发的一款Java类库扩展工具包,内含了丰富的API,涵盖了集合、缓存、并发、I/O等多个方面。使用这些AP
- 本节讲运行在后台服务里的工作请求,如何向发送请求者报告状态。推荐用LocalBroadcastManager发送和接收状态,它限制了只有本a
- Java判断一个字符串是否有中文一般情况是利用Unicode编码(CJK统一汉字的编码区间:0x4e00–0x9fbb)的正则来做判断,但是
- 总体实现思路是启动一个生产者项目注册, 将所含服务注册到zookeeper的注册中心, 然后在启动一个消费者项目,将所需服务向zookeep
- Java中的Runnable,Callable,Future,FutureTask的比较Java中存在Runnable、Callable、F
- JPA是什么? JPA(Java Persistence API)是Sun官方提出的Java持久化规范. 为Java开发人员提供了一种对象/
- 扩展阅读c#基础系列1---深入理解 值类型和引用类型c#基础系列2---深入理解 String引言在上篇文章深入理解值类型和引用类型的时候
- springmvc提供了 * ,类似于过滤器,他将在我们的请求具体出来之前先做检查,有权决定接下来是否继续,对我们的请求进行加工。 * ,可
- 芬兰数学家因卡拉花费3个月设计出了世界上迄今难度最大的数独游戏,而且它只有一个答案。因卡拉说只有思考能力最快、头脑最聪明的人才能破解这个游戏
- 本文实例讲述了C#实现对文件进行加密解密的方法。分享给大家供大家参考。具体如下:using System;using System.IO;u
- 有时候,我们需要把对象A的所有值复制给对象B(B = A),但是这样用等号给赋值你会发现,当B中的某个对象值改变时,同时也会修改到A中相应对
- String replace replaceFirst repaceAll区别replace(char oldChar, char newC
- 项目中出了个 BUG,就在我眼皮子底下,很明显的一个 BUG,愣是看了两天才看出来。我有多个任务并发,任务执行完成后都有一个返回结果,我用一
- 本文实例讲述了java生成jar包的方法,是非常实用的技巧。分享给大家供大家参考。具体分析如下:很多Java初学者都会有这样的疑问:Java
- 1、SpringMVC验证@Validated的使用第一步:编写国际化消息资源文件编写国际化消息资源ValidatedMessage.pro
- 本文实例为大家分享了Android调用外置摄像头的具体代码,供大家参考,具体内容如下1、布局文件<?xml version="
- 一、我们可以使用Spring Initializr来创建SpringBoot项目。Spring Initializr从本质上来说就是一个We
- 目录如何实现简化呢?当然,一些常见的方法已经封装在了 Norns.Urd.Extensions.Polly如何启用 Norns.Urd +
- 本文实例讲述了C#实现类似新浪微博长URL转短地址的方法。分享给大家供大家参考。具体如下:一、前台判断用户输入URL的JS代码如下。func