原理
秒杀与其他业务最大的区别在于,在秒杀的瞬间,系统的并发量和吞吐量会非常大,与此同时,网络的流量也会瞬间变大。
对于系统并发量变大问题,这里的核心在于如何在大并发的情况下保证数据库能扛得住压力,因为大并发的瓶颈在于数据库。如果用户的请求直接从前端传到数据库,显然,数据库是无法承受几十万上百万甚至上千万的并发量的。因此,我们能做的只能是减少对数据库的访问。例如,前端发出了100万个请求,通过我们的处理,最终只有10个会访问数据库,这样就会大大提升系统性能。再针对秒杀这种场景,因为秒杀商品的数量是有限的,因此这种做法刚好适用。
那么具体是如何来减少对数据库的访问的呢?
假如,某个商品可秒杀的数量是10,那么在秒杀活动开始之前,把商品的ID和数量加载到Redis缓存。当服务端收到请求时,首先预减Redis中的数量,如果数量减到小于0时,那么随后的访问直接返回秒杀失败的信息。也就是说,最终只有10个请求会去访问数据库。
如果商品数量比较多,比如1万件商品参与秒杀,那么就有1万*10=10万个请求并发去访问数据库,数据库的压力还是会很大。这里就用到了另外一个非常重要的组件:消息队列。我们不是把请求直接去访问数据库,而是先把请求写到消息队列中,做一个缓存,然后再去慢慢的更新数据库。这样做之后,前端用户的请求可能不会立即得到响应是成功还是失败,很可能得到的是一个排队中的返回值,这个时候,需要客户端去服务端轮询,因为我们不能保证一定就秒杀成功了。当服务端出队,生成订单以后,把用户ID和商品ID写到缓存中,来应对客户端的轮询就可以了。
这样处理以后,我们的应用是可以很简单的进行分布式横向扩展的,以应对更大的并发。
安装所需工具
虚拟机:docker安装mysql,rabbitmq,redis
虚拟机安装和docker安装我就不介绍了,网上都有教程。
1、docker安装mysql
[root@yk3 docker]# docker pull mysql
mysql镜像下载完成(因为我之前下载了mysql镜像,所以这里显示already exists)
使用命令:docker images查看下载的镜像
[root@yk3 docker]# docker images
红框里的就是mysql镜像。
使用mysql镜像制作容器并运行。
[root@yk3 docker]# docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root 14340cbfa999
这个14340cbfa999是我刚才下载的mysql镜像的id,也就是IMAGE ID字段对应的值,读者可以根据自己的id自行更换。
注意一定要加上-e MYSQL_ROOT_PASSWORD=数据库密码,不然启动不起来。
启动完成。使用docker ps -a命令查看运行中的容器。
使用本地的navicat连接虚拟机docker中的mysql
如果连接错误,报2058错误,解决方法:
docker进入mysql容器:
[root@yk3 docker]# docker exec -it e9023467ecfb /bin/bash
注意:-it后面跟上mysql的容器id
然后登陆mysql:
进入mysql容器执行命令:
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';
执行成功,exit返回。再用navicat连接即可成功。
2、docker安装redis
同样是docker先下载redis镜像
[root@yk3 docker]# docker pull redis
制作容器并运行:(注意镜像id改为自己的)
[root@yk3 docker]# docker run --name myredis -d -p 6379:6379 a617c1c92774
使用redis-plus连接测试:
连接成功!
3、docker安装rabbitmq
同样是docker先下载rabbitmq镜像
[root@yk3 docker]# docker pull rabbitmq
制作容器并运行:(注意镜像id改为自己的)
[root@yk3 docker]# docker run -d --name rabbit -p 15672:15672 -p 5672:5672 603fe110af88
这里要两个端口号:15672和5672,15672对应的是http(也就是我们登录RabbitMQ后台管理时用的端口),5672对应的是RabbitMQ的通信。
到这一步,rabbitmq已经启动完毕,但是在本地访问客户端http://192.168.121.130:15672这个端口还需要以下配置,
进入容器:
[root@yk3 docker]# docker exec -it 603fe110af88 /bin/bash
执行命令:rabbitmq-plugins enable rabbitmq_management便可访问
登录:用户名密码都为guest。
项目代码
新建一个springboot项目,项目结构:
application.properties:
# DB Configuration #指定数据库驱动 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #数据库jdbc连接url地址,serverTimezone设置数据库时区东八区 spring.datasource.url=jdbc:mysql://192.168.121.130:3306/mqtest?serverTimezone=GMT%2B8 #数据库账号 spring.datasource.username=root spring.datasource.password=root server.port=8990 #rabbitmq spring.rabbitmq.host=192.168.121.130 spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest #redis配置 spring.redis.host=192.168.121.130 spring.redis.port=6379 spring.redis.jedis.pool.max-active=1024 spring.redis.jedis.pool.max-wait=-1s spring.redis.jedis.pool.max-idle=200 #mybatis配置 mybatis.mapper-locations=classpath:mapper/*.xml #打印sql语句 logging.level.com.rabbitmq.mapper=debug
启动类:(启动时将库存加入到redis中)
package com.rabbitmq; import com.rabbitmq.config.RedisService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ConsumerApplication implements ApplicationRunner { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } @Autowired private RedisService redisService; /** * redis初始化各商品的库存量 * @param args * @throws Exception */ @Override public void run(ApplicationArguments args) throws Exception { redisService.put("watch", 10000, 20); } }
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 https://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.4.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.rabbitmq</groupId> <artifactId>consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>consumer</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!--jar依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> <!-- bulid插件 编译mapper层下的xml --> <resources> <resource> <directory>src/main/java/</directory> <includes> <include>com/rabbitmq/mapper/**/*.xml</include> </includes> </resource> </resources> </build> </project>
表文件:
CREATE TABLE `stock` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `stock` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; CREATE TABLE `t_order` ( `id` int NOT NULL AUTO_INCREMENT, `order_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `order_user` int DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=69406 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
对应的实体类:
stock实体类:
package com.rabbitmq.bean; import lombok.Data; import java.io.Serializable; @Data public class Stock implements Serializable { private static final long serialVersionUID = 6235666939721331057L; Integer id; String name; Integer stock; }
order实体类:
package com.rabbitmq.bean; import lombok.Data; import java.io.Serializable; @Data public class Order implements Serializable { private static final long serialVersionUID = -8271355836132430489L; Integer id; String orderName; String orderUser; }
mapper接口文件:
package com.rabbitmq.mapper; import com.rabbitmq.bean.Order; import org.apache.ibatis.annotations.Mapper; @Mapper public interface OrderMapper { Integer insert(Order order); }
package com.rabbitmq.mapper; import com.rabbitmq.bean.Stock; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface StockMapper { List<Stock> selectList(@Param("name") String name); Integer updateByPrimaryKey(Stock stock); }
mapper对应的xml配置文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.rabbitmq.mapper.OrderMapper"> <insert id="insert" parameterType="com.rabbitmq.bean.Order"> insert t_order(order_name,order_user) value (#{orderName},#{orderUser}) </insert> </mapper>
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.rabbitmq.mapper.StockMapper"> <select id="selectList" resultType="com.rabbitmq.bean.Stock"> select * from stock where name = #{name} </select> <update id="updateByPrimaryKey" parameterType="com.rabbitmq.bean.Stock"> update stock set stock = #{stock} where id = #{id}; </update> </mapper>
普通请求的service(为了方便对比,没有使用rabbitmq):
package com.rabbitmq.service; import com.rabbitmq.bean.Order; import com.rabbitmq.mapper.OrderMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderService{ @Autowired private OrderMapper orderMapper; public void createOrder(Order order) { orderMapper.insert(order); } }
package com.rabbitmq.service; import com.rabbitmq.bean.Stock; import com.rabbitmq.mapper.StockMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.List; @Service @Slf4j public class StockService { @Autowired private StockMapper stockMapper; public void decrByStock(String stockName) { synchronized(this) { List<Stock> stocks = stockMapper.selectList(stockName); if (!CollectionUtils.isEmpty(stocks)) { Stock stock = stocks.get(0); stock.setStock(stock.getStock() - 1); stockMapper.updateByPrimaryKey(stock); } } } public Integer selectByName(String stockName) { synchronized (this){ List<Stock> stocks = stockMapper.selectList(stockName); if (!CollectionUtils.isEmpty(stocks)) { return stocks.get(0).getStock().intValue(); } return 0; } } }
接下来重点来了,使用rabbitmq实现:
rabbitmq配置类:
package com.rabbitmq.config; import org.springframework.amqp.core.*; 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; @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.rabbitmq.config; 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; @Configuration public class RedisConfig { @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; } }
redis的操作类:
package com.rabbitmq.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @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); } /** * 对指定key的键值减一 * @param key * @return */ public Long decrBy(String key) { return redisTemplate.opsForValue().decrement(key); } }
重点:rabbitmq实现的service层:
package com.rabbitmq.mqservice; import com.rabbitmq.config.MyRabbitMQConfig; import com.rabbitmq.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @Slf4j public class MQOrderService { @Autowired private OrderService orderService; /** * 监听订单消息队列,并消费 * @param order */ @RabbitListener(queues = MyRabbitMQConfig.ORDER_QUEUE) public void createOrder(Order order) { log.info("收到订单消息,订单用户为:{},商品名称为:{}", order.getOrderUser(), order.getOrderName()); /** * 调用数据库orderService创建订单信息 */ orderService.createOrder(order); } }
package com.rabbitmq.mqservice; import com.rabbitmq.config.MyRabbitMQConfig; import com.rabbitmq.service.StockService; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @Slf4j public class MQStockService { @Autowired private StockService stockService; /** * 监听库存消息队列,并消费 * @param stockName */ @RabbitListener(queues = MyRabbitMQConfig.STORY_QUEUE) public void decrByStock(String stockName) { log.info("库存消息队列收到的消息商品信息是:{}", stockName); /** * 调用数据库service给数据库对应商品库存减一 */ stockService.decrByStock(stockName); } }
controller层:
package com.rabbitmq.controller; import com.rabbitmq.bean.Order; import com.rabbitmq.config.MyRabbitMQConfig; import com.rabbitmq.config.RedisService; import com.rabbitmq.service.OrderService; import com.rabbitmq.service.StockService; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @Controller @Slf4j public class SecController { @Autowired private RabbitTemplate rabbitTemplate; @Autowired private RedisService redisService; @Autowired private OrderService orderService; @Autowired private StockService stockService; /** * 使用redis+消息队列进行秒杀实现 * @param username * @param stockName * @return */ @RequestMapping("/sec") @ResponseBody public String sec(@RequestParam(value = "username") String username, @RequestParam(value = "stockName") String stockName) { log.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName); String message = null; //调用redis给相应商品库存量减一 Long decrByResult = redisService.decrBy(stockName); if (decrByResult >= 0) { /** * 说明该商品的库存量有剩余,可以进行下订单操作 */ log.info("用户:{}秒杀该商品:{}库存有余,可以进行下订单操作", username, stockName); //发消息给库存消息队列,将库存数据减一 rabbitTemplate.convertAndSend(MyRabbitMQConfig.STORY_EXCHANGE, MyRabbitMQConfig.STORY_ROUTING_KEY, stockName); //发消息给订单消息队列,创建订单 Order order = new Order(); order.setOrderName(stockName); order.setOrderUser(username); rabbitTemplate.convertAndSend(MyRabbitMQConfig.ORDER_EXCHANGE, MyRabbitMQConfig.ORDER_ROUTING_KEY, order); message = "用户" + username + "秒杀" + stockName + "成功"; } else { /** * 说明该商品的库存量没有剩余,直接返回秒杀失败的消息给用户 */ log.info("用户:{}秒杀时商品的库存量没有剩余,秒杀结束", username); message = username + "商品的库存量没有剩余,秒杀结束"; } return message; } /** * 实现纯数据库操作实现秒杀操作 * @param username * @param stockName * @return */ @RequestMapping("/secDataBase") @ResponseBody public String secDataBase(@RequestParam(value = "username") String username, @RequestParam(value = "stockName") String stockName) { synchronized (this){ redisService.decrBy(stockName); log.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName); String message = null; //查找该商品库存 Integer stockCount = stockService.selectByName(stockName); log.info("用户:{}参加秒杀,当前商品库存量是:{}", username, stockCount); if (stockCount > 0) { /** * 还有库存,可以进行继续秒杀,库存减一,下订单 */ //1、库存减一 stockService.decrByStock(stockName); //2、下订单 Order order = new Order(); order.setOrderUser(username); order.setOrderName(stockName); orderService.createOrder(order); log.info("用户:{}.参加秒杀结果是:成功", username); message = username + "参加秒杀结果是:成功"; } else { log.info("用户:{}.参加秒杀结果是:秒杀已经结束", username); message = username + "参加秒杀活动结果是:秒杀已经结束"; } return message; } } }
代码编写完毕,启动运行。
测试
使用工具 apache-jmeter-5.2.1
如何使用工具 可参考Apache JMeter5.2基础入门实践详解
- 启动
apache-jmeter-5.2.1
- 设置语言
- 创建线程组
添加http请求和监听原件
jmeter可以定义随机参数
点击绿色箭头运行