SpringBoot+Mysql+Redis+RabbitMQ实现高并发秒杀
应用场景
商城对某一商品进行秒杀活动,该项目实例中,商品为watch,库存为10,使用jemter测试工具来模拟高并发场景
搭建项目环境
开始本次测试之前,先说本次我们项目需要安装得工具以及环境,下方我提供了安装工具得博客
安装RabbitMQ
关于RabbitMQ我得上篇博客介绍得比较详细,这里也给一个安装路径
RabbitMQ安装教程
RabbitMQ安装教程
Redis+RedisDesktopManager
关于Redis缓存,Redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型)。
看到有意思得一句话,不用redis得秒杀都是耍流氓!!
下方为Redis和RedisDesktopManager安装教程
Redis和RedisDesktopManager安装教程
Jmeter压力测试工具
Jmeter为基于java得压力测试工具,本文用它模仿40个不同得用户同时发送请求秒杀商品,下方为Jmeter得安装教程
Jmeter安装教程
数据库表结构
编写代码
配置pom.xml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| 1 <dependencies>
2 <dependency>
3 <groupId>org.springframework.boot</groupId>
4 <artifactId>spring-boot-starter-web</artifactId>
5 </dependency>
6 <dependency>
7 <groupId>org.springframework.boot</groupId>
8 <artifactId>spring-boot-starter-test</artifactId>
9 <scope>test</scope>
10 <exclusions>
11 <exclusion>
12 <groupId>org.junit.vintage</groupId>
13 <artifactId>junit-vintage-engine</artifactId>
14 </exclusion>
15 </exclusions>
16 </dependency>
17 <dependency>
18 <groupId>org.projectlombok</groupId>
19 <artifactId>lombok</artifactId>
20 <optional>true</optional>
21 </dependency>
22 <dependency>
23 <groupId>org.springframework.boot</groupId>
24 <artifactId>spring-boot-starter-data-jpa</artifactId>
25 </dependency>
26 <dependency>
27 <groupId>org.springframework.boot</groupId>
28 <artifactId>spring-boot-devtools</artifactId>
29 <scope>runtime</scope>
30 <optional>true</optional>
31 </dependency>
32 <dependency>
33 <groupId>mysql</groupId>
34 <artifactId>mysql-connector-java</artifactId>
35 <scope>runtime</scope>
36 </dependency>
37 <dependency>
38 <groupId>org.apache.commons</groupId>
39 <artifactId>commons-lang3</artifactId>
40 <version>3.8.1</version>
41 </dependency>
42 <dependency>
43 <groupId>org.springframework.boot</groupId>
44 <artifactId>spring-boot-starter-amqp</artifactId>
45 </dependency>
46 <dependency>
47 <groupId>io.jsonwebtoken</groupId>
48 <artifactId>jjwt</artifactId>
49 <version>0.7.0</version>
50 </dependency>
51 <dependency>
52 <groupId>org.springframework.boot</groupId>
53 <artifactId>spring-boot-starter-data-redis</artifactId>
54 </dependency>
55 <dependency>
56 <groupId>org.mybatis.spring.boot</groupId>
57 <artifactId>mybatis-spring-boot-starter</artifactId>
58 <version>2.1.0</version>
59 </dependency>
60 <dependency>
61 <groupId>org.springframework</groupId>
62 <artifactId>spring-tx</artifactId>
63 </dependency>
64 <dependency>
65 <groupId>tk.mybatis</groupId>
66 <artifactId>mapper-spring-boot-starter</artifactId>
67 <version>2.0.3-beta1</version>
68 </dependency>
69 <dependency>
70 <groupId>tk.mybatis</groupId>
71 <artifactId>mapper</artifactId>
72 <version>4.0.0</version>
73 </dependency>
74 </dependencies>
75
76 |
配置application.properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 1 spring.devtools.restart.enabled=false
2 server.port=8443
3 ##配置数据库连接
4 spring.datasource.username=root
5 spring.datasource.password=root
6 spring.datasource.url=jdbc:mysql://localhost:3306/ktoa?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true
7 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
8
9 ##配置rabbitmq连接
10 spring.rabbitmq.host=localhost
11 spring.rabbitmq.port=5672
12 spring.rabbitmq.username=guest
13 spring.rabbitmq.password=guest
14
15 ##配置连接redis --都记得打开服务
16 spring.redis.host=localhost
17 spring.redis.port=6379
18 spring.redis.jedis.pool.max-active=1024
19 spring.redis.jedis.pool.max-wait=-1s
20 spring.redis.jedis.pool.max-idle=200
21 spring.redis.password=123456
22
23 |
添加实体类
Order.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 1 import lombok.Data;
2 import javax.persistence.Column;
3 import javax.persistence.GeneratedValue;
4 import javax.persistence.GenerationType;
5 import javax.persistence.Id;
6 import javax.persistence.Table;
7 import java.io.Serializable;
8 @Data
9 @Table(name = "t_order")
10 public class Order implements Serializable {
11 private static final long serialVersionUID = -8867272732777764701L;
12
13 @Id
14 @Column(name = "id")
15 @GeneratedValue(strategy = GenerationType.IDENTITY)
16 private Long id;
17
18 @Column(name = "order_name")
19 private String order_name;
20
21 @Column(name = "order_user")
22 private String order_user;
23 }
24
25
26
27 |
Stock.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 1 import lombok.Data;
2 import javax.persistence.Column;
3 import javax.persistence.GeneratedValue;
4 import javax.persistence.GenerationType;
5 import javax.persistence.Id;
6 import javax.persistence.Table;
7 import java.io.Serializable;
8 @Table(name = "stock")
9 @Data
10 public class Stock implements Serializable {
11 private static final long serialVersionUID = 2451194410162873075L;
12
13 @Id
14 @Column(name = "id")
15 @GeneratedValue(strategy = GenerationType.IDENTITY)
16 private Long id;
17
18 @Column(name = "name")
19 private String name;
20
21 @Column(name = "stock")
22 private Long stock;
23 }
24
25 |
. 配置tkmybatis得接口
新建名为base得包,在base下面新建service得接口
1 2 3 4 5 6
| 1 import tk.mybatis.mapper.common.Mapper;
2 import tk.mybatis.mapper.common.MySqlMapper;
3 public interface GenericMapper<T> extends Mapper<T>, MySqlMapper<T> {
4 }
5
6 |
关于这个接口得作用你不需要了解太多,你只要知道我们得mapper层需要通过继承它来实现数据库操作,如果你接触过jpa或者mybatis-plus,tkmybatis方式跟它们相似。
新建mapper层
新建名为mapper得包,在这个包下面新建
OrderMapper
1 2 3 4 5 6 7 8 9
| 1 import com.spbtrediskill.secondskill.base.service.GenericMapper;
2 import com.spbtrediskill.secondskill.pojo.Order;
3 import org.apache.ibatis.annotations.Mapper;
4 @Mapper
5 public interface OrderMapper extends GenericMapper<Order> {
6 void insertOrder(Order order);
7 }
8
9 |
StockMapper
1 2 3 4 5 6 7 8
| 1 import com.spbtrediskill.secondskill.base.service.GenericMapper;
2 import com.spbtrediskill.secondskill.pojo.Stock;
3 import org.apache.ibatis.annotations.Mapper;
4 @Mapper
5 public interface StockMapper extends GenericMapper<Stock> {
6 }
7
8 |
编写RabbitMQ和redis得配置类
新建config包,新建redis和RabbitMQ得类
MyRabbitMQConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| 1 import org.springframework.amqp.core.Binding;
2 import org.springframework.amqp.core.BindingBuilder;
3 import org.springframework.amqp.core.Exchange;
4 import org.springframework.amqp.core.ExchangeBuilder;
5 import org.springframework.amqp.core.Queue;
6 import org.springframework.amqp.core.QueueBuilder;
7 import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
8 import org.springframework.amqp.support.converter.MessageConverter;
9 import org.springframework.context.annotation.Bean;
10 import org.springframework.context.annotation.Configuration;
11 import java.util.HashMap;
12 import java.util.Map;
13 @Configuration
14 public class MyRabbitMQConfig {
15
16 ```
17 //库存交换机
18 public static final String STORY_EXCHANGE = "STORY_EXCHANGE";
19
20 //订单交换机
21 public static final String ORDER_EXCHANGE = "ORDER_EXCHANGE";
22
23 //库存队列
24 public static final String STORY_QUEUE = "STORY_QUEUE";
25
26 //订单队列
27 public static final String ORDER_QUEUE = "ORDER_QUEUE";
28
29 //库存路由键
30 public static final String STORY_ROUTING_KEY = "STORY_ROUTING_KEY";
31
32 //订单路由键
33 public static final String ORDER_ROUTING_KEY = "ORDER_ROUTING_KEY";
34 @Bean
35 public MessageConverter messageConverter() {
36 return new Jackson2JsonMessageConverter();
37 }
38 //创建库存交换机
39 @Bean
40 public Exchange getStoryExchange() {
41 return ExchangeBuilder.directExchange(STORY_EXCHANGE).durable(true).build();
42 }
43 //创建库存队列
44 @Bean
45 public Queue getStoryQueue() {
46 return new Queue(STORY_QUEUE);
47 }
48 //库存交换机和库存队列绑定
49 @Bean
50 public Binding bindStory() {
51 return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs();
52 }
53 //创建订单队列
54 @Bean
55 public Queue getOrderQueue() {
56 return new Queue(ORDER_QUEUE);
57 }
58 //创建订单交换机
59 @Bean
60 public Exchange getOrderExchange() {
61 return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
62 }
63 //订单队列与订单交换机进行绑定
64 @Bean
65 public Binding bindOrder() {
66 return BindingBuilder.bind(getOrderQueue()).to(getOrderExchange()).with(ORDER_ROUTING_KEY).noargs();
67 }
68
69 }
70
71 |
RedisConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 1 import org.springframework.context.annotation.Bean;
2 import org.springframework.context.annotation.Configuration;
3 import org.springframework.data.redis.connection.RedisConnectionFactory;
4 import org.springframework.data.redis.core.RedisTemplate;
5 import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
6 import org.springframework.data.redis.serializer.StringRedisSerializer;
7
8 @Configuration
9 public class RedisConfig {
10 // 配置redis得配置详解
11 @Bean
12 public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
13 RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
14 template.setConnectionFactory(redisConnectionFactory);
15 template.setKeySerializer(new StringRedisSerializer());
16 template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
17 template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
18 template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
19 template.afterPropertiesSet();
20 return template;
21 }
22 }
23
24 |
service层
OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 1 import com.spbtrediskill.secondskill.mapper.OrderMapper;
2 import com.spbtrediskill.secondskill.pojo.Order;
3 import com.spbtrediskill.secondskill.service.OrderService;
4 import org.springframework.beans.factory.annotation.Autowired;
5 import org.springframework.stereotype.Service;
6
7 @Service
8 public class OrderServiceImpl implements OrderService {
9 @Autowired
10 private OrderMapper orderMapper;
11 @Override
12 public void createOrder(Order order) {
13 orderMapper.insert(order);
14 }
15 }
16
17
18 |
StockServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| 1 import com.spbtrediskill.secondskill.mapper.StockMapper;
2 import com.spbtrediskill.secondskill.pojo.Stock;
3 import com.spbtrediskill.secondskill.service.StockService;
4 import org.springframework.beans.factory.annotation.Autowired;
5 import org.springframework.stereotype.Service;
6 import org.springframework.util.CollectionUtils;
7 import tk.mybatis.mapper.entity.Example;
8 import java.util.List;
9
10 @Service
11 public class StockServiceImpl implements StockService {
12 @Autowired
13 private StockMapper stockMapper;
14 // 秒杀商品后减少库存
15 @Override
16 public void decrByStock(String stockName) {
17 Example example = new Example(Stock.class);
18 Example.Criteria criteria = example.createCriteria();
19 criteria.andEqualTo("name", stockName);
20 List<Stock> stocks = stockMapper.selectByExample(example);
21 if (!CollectionUtils.isEmpty(stocks)) {
22 Stock stock = stocks.get(0);
23 stock.setStock(stock.getStock() - 1);
24 stockMapper.updateByPrimaryKey(stock);
25 }
26 }
27 // 秒杀商品前判断是否有库存
28 @Override
29 public Integer selectByExample(String stockName) {
30 Example example = new Example(Stock.class);
31 Example.Criteria criteria = example.createCriteria();
32 criteria.andEqualTo("name", stockName);
33 List<Stock> stocks = stockMapper.selectByExample(example);
34 if (!CollectionUtils.isEmpty(stocks)) {
35 return stocks.get(0).getStock().intValue();
36 }
37 return 0;
38 }
39 }
40
41 |
MQOrderService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 1 import com.spbtrediskill.secondskill.config.MyRabbitMQConfig;
2 import com.spbtrediskill.secondskill.pojo.Order;
3 import lombok.extern.slf4j.Slf4j;
4 import org.springframework.amqp.rabbit.annotation.RabbitListener;
5 import org.springframework.beans.factory.annotation.Autowired;
6 import org.springframework.stereotype.Service;
7 @Service
8 @Slf4j
9 public class MQOrderService {
10 @Autowired
11 private OrderService orderService;
12 /**
13 * 监听订单消息队列,并消费
14 *
15 * @param order
16 */
17 @RabbitListener(queues = MyRabbitMQConfig.ORDER_QUEUE)
18 public void createOrder(Order order) {
19 log.info("收到订单消息,订单用户为:{},商品名称为:{}", order.getOrder_user(), order.getOrder_name());
20 /**
21 * 调用数据库orderService创建订单信息
22 */
23 orderService.createOrder(order);
24 }
25 }
26
27 |
MQStockService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 1 import com.spbtrediskill.secondskill.config.MyRabbitMQConfig;
2 import lombok.extern.slf4j.Slf4j;
3 import org.springframework.amqp.rabbit.annotation.RabbitListener;
4 import org.springframework.beans.factory.annotation.Autowired;
5 import org.springframework.stereotype.Service;
6 @Service
7 @Slf4j
8 public class MQStockService {
9 @Autowired
10 private StockService stockService;
11 /**
12 * 监听库存消息队列,并消费
13 * @param stockName
14 */
15 @RabbitListener(queues = MyRabbitMQConfig.STORY_QUEUE)
16 public void decrByStock(String stockName) {
17 log.info("库存消息队列收到的消息商品信息是:{}", stockName);
18 /**
19 * 调用数据库service给数据库对应商品库存减一
20 */
21 stockService.decrByStock(stockName);
22 }
23 }
24
25 |
RedisService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| 1 import org.springframework.beans.factory.annotation.Autowired;
2 import org.springframework.data.redis.core.RedisTemplate;
3 import org.springframework.stereotype.Service;
4 import java.util.Date;
5 import java.util.concurrent.TimeUnit;
6 @Service
7 public class RedisService {
8 @Autowired
9 private RedisTemplate<String, Object> redisTemplate;
10 /**
11 * 设置String键值对
12 * @param key
13 * @param value
14 * @param millis
15 */
16 public void put(String key, Object value, long millis) {
17 redisTemplate.opsForValue().set(key, value, millis, TimeUnit.MINUTES);
18 }
19 public void putForHash(String objectKey, String hkey, String value) {
20 redisTemplate.opsForHash().put(objectKey, hkey, value);
21 }
22 public <T> T get(String key, Class<T> type) {
23 return (T) redisTemplate.boundValueOps(key).get();
24 }
25 public void remove(String key) {
26 redisTemplate.delete(key);
27 }
28 public boolean expire(String key, long millis) {
29 return redisTemplate.expire(key, millis, TimeUnit.MILLISECONDS);
30 }
31 public boolean persist(String key) {
32 return redisTemplate.hasKey(key);
33 }
34 public String getString(String key) {
35 return (String) redisTemplate.opsForValue().get(key);
36 }
37 public Integer getInteger(String key) {
38 return (Integer) redisTemplate.opsForValue().get(key);
39 }
40 public Long getLong(String key) {
41 return (Long) redisTemplate.opsForValue().get(key);
42 }
43 public Date getDate(String key) {
44 return (Date) redisTemplate.opsForValue().get(key);
45 }
46
47 /**
48 * 对指定key的键值减一
49 * @param key
50 * @return
51 */
52 public Long decrBy(String key) {
53 return redisTemplate.opsForValue().decrement(key);
54 }
55 }
56
57 |
controller
MQOrderService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| 1 import com.spbtrediskill.secondskill.config.MyRabbitMQConfig;
2 import com.spbtrediskill.secondskill.pojo.Order;
3 import com.spbtrediskill.secondskill.service.OrderService;
4 import com.spbtrediskill.secondskill.service.RedisService;
5 import com.spbtrediskill.secondskill.service.StockService;
6 import lombok.extern.slf4j.Slf4j;
7 import org.springframework.amqp.rabbit.core.RabbitTemplate;
8 import org.springframework.beans.factory.annotation.Autowired;
9 import org.springframework.stereotype.Controller;
10 import org.springframework.web.bind.annotation.PostMapping;
11 import org.springframework.web.bind.annotation.RequestMapping;
12 import org.springframework.web.bind.annotation.RequestParam;
13 import org.springframework.web.bind.annotation.ResponseBody;
14 @Controller
15 @Slf4j
16 public class SecController {
17 @Autowired
18 private RabbitTemplate rabbitTemplate;
19 @Autowired
20 private RedisService redisService;
21 @Autowired
22 private OrderService orderService;
23 @Autowired
24 private StockService stockService;
25 /**
26 * 使用redis+消息队列进行秒杀实现
27 *
28 * @param username
29 * @param stockName
30 * @return
31 */
32 @PostMapping( value = "/sec",produces = "application/json;charset=utf-8")
33 @ResponseBody
34 public String sec(@RequestParam(value = "username") String username, @RequestParam(value = "stockName") String stockName) {
35
36 log.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName);
37 String message = null;
38 //调用redis给相应商品库存量减一
39 Long decrByResult = redisService.decrBy(stockName);
40 if (decrByResult >= 0) {
41 /**
42 * 说明该商品的库存量有剩余,可以进行下订单操作
43 */
44 log.info("用户:{}秒杀该商品:{}库存有余,可以进行下订单操作", username, stockName);
45 //发消息给库存消息队列,将库存数据减一
46 rabbitTemplate.convertAndSend(MyRabbitMQConfig.STORY_EXCHANGE, MyRabbitMQConfig.STORY_ROUTING_KEY, stockName);
47
48 //发消息给订单消息队列,创建订单
49 Order order = new Order();
50 order.setOrder_name(stockName);
51 order.setOrder_user(username);
52 rabbitTemplate.convertAndSend(MyRabbitMQConfig.ORDER_EXCHANGE, MyRabbitMQConfig.ORDER_ROUTING_KEY, order);
53 message = "用户" + username + "秒杀" + stockName + "成功";
54 } else {
55 /**
56 * 说明该商品的库存量没有剩余,直接返回秒杀失败的消息给用户
57 */
58 log.info("用户:{}秒杀时商品的库存量没有剩余,秒杀结束", username);
59 message = "用户:"+ username + "商品的库存量没有剩余,秒杀结束";
60 }
61 return message;
62 }
63 }
64
65
66 |
编写springboot启动类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 1 import com.spbtrediskill.secondskill.service.RedisService;
2 import org.springframework.beans.factory.annotation.Autowired;
3 import org.springframework.boot.SpringApplication;
4 import org.springframework.boot.autoconfigure.SpringBootApplication;
5 import org.springframework.boot.ApplicationArguments;
6 import org.springframework.boot.ApplicationRunner;
7 import tk.mybatis.spring.annotation.MapperScan;
8 @SpringBootApplication
9 @MapperScan("com.spbtrediskill.secondskill.mapper")
10 public class SecondskillApplication implements ApplicationRunner{
11 public static void main(String[] args) {
12 SpringApplication.run(SecondskillApplication.class, args);
13 }
14 @Autowired
15 private RedisService redisService;
16 /**
17 * redis初始化商品的库存量和信息
18 * @param args
19 * @throws Exception
20 */
21 @Override
22 public void run(ApplicationArguments args) throws Exception {
23 redisService.put("watch", 10, 20);
24 }
25 }
26
27 |
tion.MapperScan;
@SpringBootApplication
@MapperScan(“com.spbtrediskill.secondskill.mapper”)
public class SecondskillApplication implements ApplicationRunner{
public static void main(String[] args) {
SpringApplication.run(SecondskillApplication.class, args);
}
@Autowired
private RedisService redisService;
/**
* redis初始化商品的库存量和信息
* @param args
* @throws Exception
*/
@Override
public void run(ApplicationArguments args) throws Exception {
redisService.put(“watch”, 10, 20);
}
}