文章前言
眾所周知,當遇到比較多數據不一致的問題時,大多數都是因為並發請求時,沒及時處理的原因,提一個電商平台比較經常出現得高並發場景限時秒殺活動,他們是怎么來防止超賣呢?如何實現高並發秒殺呢?。
本文模擬了高並發秒殺,並且防止了超賣,也模擬了純數據庫秒殺超賣得場景,本次模擬demo得框架技術為:SpringBoot+Mysql+Redis+RabbitMQ+tkmybatis
數據庫表結構:




一個為庫存表,一個為訂單表,本人使用得是mysql8.0。
完整得項目工具展示
Jmeter :

redisManager :

RabbitMQ :

編寫代碼
1.首先新建Springboot項目

2.可以先不勾選需要得jar包,項目初始化好之后,使用maven導入項目需要得jar包
pom.xml :
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</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</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.3-beta1</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>
3.配置application.properties
spring.devtools.restart.enabled=false ##配置數據庫連接 spring.datasource.username=root spring.datasource.password=root server.port=8443 spring.datasource.url=jdbc:mysql://localhost:3306/ktoa?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ##配置rabbitmq連接 spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest ##配置連接redis --都記得打開服務 spring.redis.host=localhost 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 spring.redis.password=123456
這時可以啟動一下springboot項目是否能夠正常啟動,如沒問題可以繼續往下編寫!!
4.新建pojo包,添加實體類
Order.java:
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
@Data
@Table(name = "t_order")
public class Order implements Serializable {
private static final long serialVersionUID = -8867272732777764701L;
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_name")
private String order_name;
@Column(name = "order_user")
private String order_user;
}
Stock.java:
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
@Table(name = "stock")
@Data
public class Stock implements Serializable {
private static final long serialVersionUID = 2451194410162873075L;
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "stock")
private Long stock;
}
因為本次數據庫操作方面使用了tkmybatis框架,所以實體類我們需要用到JPA的注解,來實現映射關系!!

5.配置tkmybatis得接口
新建名為base得包,在base下面新建service得接口

GenericMapper.interface:
import tk.mybatis.mapper.common.Mapper;
import tk.mybatis.mapper.common.MySqlMapper;
public interface GenericMapper<T> extends Mapper<T>, MySqlMapper<T> {
}
關於這個接口得作用你需要了解太多,你只要知道我們得mapper層需要通過繼承它來實現數據庫操作,如果你接觸過jpa或者mybatis-plus,tkmybatis方式跟它們相似。
6.新建mapper層
新建名為mapper得包,在這個包下面新建

OrderMapper.interface:
import com.spbtrediskill.secondskill.base.service.GenericMapper;
import com.spbtrediskill.secondskill.pojo.Order;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderMapper extends GenericMapper<Order> {
void insertOrder(Order order);
}
StockMapper.interface:
import com.spbtrediskill.secondskill.base.service.GenericMapper;
import com.spbtrediskill.secondskill.pojo.Stock;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface StockMapper extends GenericMapper<Stock> {
}
7.編寫RabbitMQ和redis得配置類
新建config包,新建redis和RabbitMQ得類
MyRabbitMQConfig.java:
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.core.QueueBuilder;
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;
import java.util.HashMap;
import java.util.Map;
@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();
}
}
RedisConfig .java:
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 {
// 配置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;
}
}
8.編寫service層
新建service包以及impl包,這里只提供實現類,接口可以自行編寫
OrderServiceImpl .java:
import com.spbtrediskill.secondskill.mapper.OrderMapper;
import com.spbtrediskill.secondskill.pojo.Order;
import com.spbtrediskill.secondskill.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Override
public void createOrder(Order order) {
orderMapper.insert(order);
}
}
StockServiceImpl.java:
import com.spbtrediskill.secondskill.mapper.StockMapper;
import com.spbtrediskill.secondskill.pojo.Stock;
import com.spbtrediskill.secondskill.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import tk.mybatis.mapper.entity.Example;
import java.util.List;
@Service
public class StockServiceImpl implements StockService {
@Autowired
private StockMapper stockMapper;
// 秒殺商品后減少庫存
@Override
public void decrByStock(String stockName) {
Example example = new Example(Stock.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("name", stockName);
List<Stock> stocks = stockMapper.selectByExample(example);
if (!CollectionUtils.isEmpty(stocks)) {
Stock stock = stocks.get(0);
stock.setStock(stock.getStock() - 1);
stockMapper.updateByPrimaryKey(stock);
}
}
// 秒殺商品前判斷是否有庫存
@Override
public Integer selectByExample(String stockName) {
Example example = new Example(Stock.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("name", stockName);
List<Stock> stocks = stockMapper.selectByExample(example);
if (!CollectionUtils.isEmpty(stocks)) {
return stocks.get(0).getStock().intValue();
}
return 0;
}
}
9.配置rabbitmq得實現方式以及redis得實現方式
在 service包下面新建 MQOrderService.java
這個類屬於訂單得消費隊列
import com.spbtrediskill.secondskill.config.MyRabbitMQConfig;
import com.spbtrediskill.secondskill.pojo.Order;
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.getOrder_user(), order.getOrder_name());
/**
* 調用數據庫orderService創建訂單信息
*/
orderService.createOrder(order);
}
}
MQStockService.java:
這個屬於庫存得消費隊列
import com.spbtrediskill.secondskill.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;
@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);
}
}
RedisService.java:
這個配置類,主要用來實現對redis得key和value初始化以及對value得操作
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;
@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);
}
}
下面為service包得完整目錄:

10.編寫controller層
在新建得controller包下面新建類 SecController.java
該controller提供了二個方法,一個為redis+rabbitmq實現高並發秒殺,第二個則用純數據庫模擬秒殺,出現超賣現象
import com.spbtrediskill.secondskill.config.MyRabbitMQConfig;
import com.spbtrediskill.secondskill.pojo.Order;
import com.spbtrediskill.secondskill.service.OrderService;
import com.spbtrediskill.secondskill.service.RedisService;
import com.spbtrediskill.secondskill.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.PostMapping;
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
*/
@PostMapping( value = "/sec",produces = "application/json;charset=utf-8")
@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.setOrder_name(stockName);
order.setOrder_user(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) {
log.info("參加秒殺的用戶是:{},秒殺的商品是:{}", username, stockName);
String message = null;
//查找該商品庫存
Integer stockCount = stockService.selectByExample(stockName);
log.info("用戶:{}參加秒殺,當前商品庫存量是:{}", username, stockCount);
if (stockCount > 0) {
/**
* 還有庫存,可以進行繼續秒殺,庫存減一,下訂單
*/
//1、庫存減一
stockService.decrByStock(stockName);
//2、下訂單
Order order = new Order();
order.setOrder_user(username);
order.setOrder_name(stockName);
orderService.createOrder(order);
log.info("用戶:{}.參加秒殺結果是:成功", username);
message = username + "參加秒殺結果是:成功";
} else {
log.info("用戶:{}.參加秒殺結果是:秒殺已經結束", username);
message = username + "參加秒殺活動結果是:秒殺已經結束";
}
return message;
}
11.編寫springboot啟動類
最后一步我們需要在springboot得啟動類中進行對redis得初始化,簡而言之就是調用我們上面寫得方法,新建一個redis緩存,模擬商品信息
import com.spbtrediskill.secondskill.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import tk.mybatis.spring.annotation.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);
}
}
項目得整個目錄:

至此我們得項目代碼就編寫完成了,記得仔細檢查是否有遺漏,下面准備進入最重要得測試環節!!
測試前提
上面代碼編寫完整之后我們可以啟動springboot,啟動之前記得打開redis和rabbitmq得服務,檢查是否出錯:

啟動成功之后打開Redis Desktop Manager工具,查看是否新建了一個redis :watch、

ok,如果好了,現在打開我們得JMeter工具,可能有些人對這個工具很陌生,下面我教大家如何使用JMeter,大佬忽略!!
首先選擇中文

完成中文之后,我們在測試計划右鍵,添加一個線程組


給這個線程組得數量為40,這個線程組得作用就是模擬40個用戶發送請求,去秒殺.
然后再在線程組右鍵,添加一個Http請求,這個就是我們用來發送請求得組件了


這個請求唯一要說得就是,隨機參數了,因為用戶名肯定不可能給40個相同得名字,這邊我們利用JMeter給用戶名得值為隨機數
點擊上方得白色小書本,選擇random,1-99得隨機數:

然后我們把這個函數字符串復制到http得參數上面去:

最后我們在測試計划建一個結果樹,查看我們發送請求返回得消息數據:

這些完成之后我們就可以開始發送請求了
運行run
測試結果–redis+rabbitmq
運行之后查看我們得控制台:

可以看到日志已經打印到控制台了,用戶名為我們生成得隨機數。
再來看下數據庫訂單表order:

圖中有10條秒殺到商品得用戶信息和商品名,我再幫大家理一理,我們初始化得時候給watch庫存得數量為10,而我們使用JMeter模擬了40個人發請求,所以這10條數據,也就是40個用戶中搶到商品得10個人,也就是線程,誰搶到就是誰得。
再來查看下我們得結果樹:


結果樹上面有40條請求信息,通過其中我們可以看的每條請求得詳細數據以及返回得值。
現在我們再打開redismanager,其中我們初始化為10,現在是-30,可以知道有40個線程去獲取了它,現在為-30,每次前測試記得,手動清空緩存!!一定要記得


純數據庫方式秒殺結果
上面我們實現了redis+rabbitmq得秒殺,現在我們看看純數據庫方式得秒殺,看看有什么區別:
1.首先網stock庫存表新增一條數據,類似於redis得初始化
2.在jmeter中修改原來得http請求信息,其中小米對應數據庫得商品名

清空一下結果樹,我們開始運行
3.run
控制台:

重要得是查看數據庫得信息:
庫存已經清空,再看order表
這樣我們可以看到,明明只有10個庫存得商品,搶到得人卻不止10個,這樣明細超賣了,請求樹也可以看的超賣信息
總結
從這二個方式實現得秒殺就可以知道二者得區別,以及大概得了解這個過程是怎么實現得,寫這篇文章得主要初衷是方便那些剛接觸這方面得小白,沒有人剛來什么都會。
