场景:
商城对某一商品进行秒杀活动,该项目实例中,商品为watch,库存为10,使用jemter测试工具来模拟高并发场景
代码实例:
mysql表结构:
库存表: 订单表:
application.yml文件
server:
port: 8089
spring:
rabbitmq:
virtual-host: /
host: localhost
username: guest
password: guest
application:
name: concurrency-project
redis:
host: localhost
port: 6379
jedis:
pool:
max-active: 1024
max-wait: -1s
max-idle: 200
password: 123456
datasource:
name: db_concurrency
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_concurrency
username: root
password:
initial-size: 1
min-idle: 1
max-active: 20
#获取连接等待超时时间
max-wait: 60000
#间隔多久进行一次检测,检测需要关闭的空闲连接
time-between-eviction-runs-millis: 60000
#一个连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
mybatis:
mapper-locations: mapper/*.xml
type-aliases-package: com.concurrency.concurrencyproject.model
pagehelper:
helper-dialect: mysql
mapper:
mappers: com.concurrency.concurrencyproject.base.service.GenericMapper
not-empty: false
identity: MYSQL
项目启动类:
@SpringBootApplication
@MapperScan("com.concurrency.concurrencyproject.mapper")
public class ConcurrencyProjectApplication implements ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(ConcurrencyProjectApplication.class, args);
}
@Autowired
private RedisService redisService;
/**
* redis初始化各商品的库存量
*
* @param args
* @throws Exception
*/
@Override
public void run(ApplicationArguments args) throws Exception {
redisService.put("watch", 10, 20);
}
}
启动之后redis初始化为:
RabbitMQ配置类:
@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 配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
Redis service
@Service
public class RedisService {
@Autowired
private RedisTemplate 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 get(String key, Class 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);
}
}
Controller
@Controller
public class SecController {
private Logger LOGGER = LoggerFactory.getLogger(getClass());
@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) {
LOGGER.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName);
String message = null;
//调用redis给相应商品库存量减一
Long decrByResult = redisService.decrBy(stockName);
if (decrByResult >= 0) {
/**
* 说明该商品的库存量有剩余,可以进行下订单操作
*/
LOGGER.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 {
/**
* 说明该商品的库存量没有剩余,直接返回秒杀失败的消息给用户
*/
LOGGER.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) {
LOGGER.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName);
String message = null;
//查找该商品库存
Integer stockCount = stockService.selectByExample(stockName);
LOGGER.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);
LOGGER.info("用户:{}.参加秒杀结果是:成功", username);
message = username + "参加秒杀结果是:成功";
} else {
LOGGER.info("用户:{}.参加秒杀结果是:秒杀已经结束", username);
message = username + "参加秒杀活动结果是:秒杀已经结束";
}
return message;
}
}
RabbitMQ Service
@Service
public class MQStockService {
private Logger LOGGER = LoggerFactory.getLogger(getClass());
@Autowired
private StockService stockService;
/**
* 监听库存消息队列,并消费
*
* @param stockName
*/
@RabbitListener(queues = MyRabbitMQConfig.STORY_QUEUE)
public void decrByStock(String stockName) {
LOGGER.info("库存消息队列收到的消息商品信息是:{}", stockName);
/**
* 调用数据库service给数据库对应商品库存减一
*/
stockService.decrByStock(stockName);
}
}
@Service
public class MQOrderService {
private Logger LOGGER = LoggerFactory.getLogger(getClass());
@Autowired
private OrderService orderService;
/**
* 监听订单消息队列,并消费
*
* @param order
*/
@RabbitListener(queues = MyRabbitMQConfig.ORDER_QUEUE)
public void createOrder(Order order) {
LOGGER.info("收到订单消息,订单用户为:{},商品名称为:{}", order.getOrder_user(), order.getOrder_name());
/**
* 调用数据库orderService创建订单信息
*/
orderService.createOrder(order);
}
}
数据库Service
@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 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 stocks = stockMapper.selectByExample(example);
if (!CollectionUtils.isEmpty(stocks)) {
return stocks.get(0).getStock().intValue();
}
return 0;
}
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Override
public void createOrder(Order order) {
orderMapper.insert(order);
}
}
测试:使用jemter测试,40个线程并发测试(也可以更多)
线程组:
测试参数文件:
先测试使用redis+rabbitmq实现高并发秒杀的代码:
http请求:
点击启动,项目日志打印:
redis数据:
数据库数据:
jemeter 察看结果树:
jemeter 聚合报告:
可以看出,使用redis+rabbitmq可以实现高并发场景下的秒杀任务
接下来用纯数据库操作实现高并发场景下的秒杀任务:
将http请求中的地址换成:/secDataBase , 其他地方不改变;同时清空数据库订单表的数据并将商品的库存量初始化为10
启动测试,日志打印如下:
可以发现,由于并发的操作,导致多个请求进来的时候读取的商品库存量都是一样的
数据库的数据:
可以看出,由于并发的情况,导致订单超卖,所有用户都已经下单成功了,并且数据库中的库存也并未归零(随着并发量的增多,数据库中的库存会为0 ,但是会有更多的用户下单成功,也会有用户下单失败,秒杀结束)
jemeter的察看结果树中http返回都是用户下单成功:
jemeter的聚合报告如下:
至此,这里使用了redis+rabbitmq实现了高并发秒杀场景,并有效的防止了超卖现象,同时听过使用纯数据库操作产生了超卖的现象。