目录

流程

接口说明

1. 业务逻辑层 / Controller

2. mapper

3. mq配置

3.1. rabbitmq配置类

3.2. 生产者

3.3. 消费者

4. 表结构

5. 测试

5.1. 给商品库存id为2的添加100条库存

5.2. 添加库存数量到redis

5.3. jmeter多线程模拟抢购

5.4. jmeter执行结束后查看库存的变化

5.5. 查询是否抢购成功 --(前台ajax定时实时查询此接口)

6. 生产者和消费者要分两个服务


流程

1. 在后台把商品库存保存到redis中,采用list命令(获取特性:从左获取并移除),有多少库存就for循环多少次添加到list中,保存格式:(库存id,for循环中的自增id),
2. 用户请求抢购接口
2.1 使用redis的setnx命令限制用户5秒内只能请求一次
2.2 获取redis中提前存好的库存,能获取到就代表抢购成功,执行以下流程,反之返回该商品已售完。

2.2.1 生产者 - 走到这一步的用户代表已经抢到该商品,把用户id、库存id放入mq消息队列中

2.2.2 消费者 - 抽取以下的subtractInventory()和addInventoryLog()方法,把减库存和添加用户到抢购成功记录表的方法抽出当做消费者来异步消费第1步

2.3 去数据库修改库存数量,采用行锁/乐观锁防止库存<0,sql:update 库存数量 set 库存数量 = 库存数量 - 1 where 库存id = #{库存id} and 库存数量 > 0 (v1版本最后讲到了乐观锁的使用,及乐观锁和行锁的区别。 >>>移步到v1版本
2.4 修改库存成功后,异步保存当前'用户id'及'库存id'保存到用户抢购成功记录表中,
2.5 前端定时实时ajax请求后台“查询用户抢购成功记录表接口”是否抢购成功。

接口说明

1. 后台生成商品库存到redis:http://127.0.0.1:81/addInventoryToRedis?inventoryId=110&quantity=100
2. 抢购接口:http://127.0.0.1:81/subtractInventory?phone=18713901666&inventoryId=110
3. 查询是否抢购成功:http://127.0.0.1:81/getInventoryLog?phone=18713901666&inventoryId=110

1. 业务逻辑层 / Controller

package com.chuangqi.defense.controller;

import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.chuangqi.defense.mapper.Test_01Mapper;
import com.chuangqi.defense.mq.ProducerQueue;
import cn.hutool.json.JSONObject;
import lombok.extern.log4j.Log4j2;

/**
 * 抢购逻辑
 *
 * @author qizhentao
 * @version V1.0
 */
@Log4j2
@RestController
public class Test_01 {

	@Resource
	private RedisTemplate<String, Object> redisTemplate;
	
	@Autowired
	private Test_01Mapper mapper;
	
        // MQ生产者队列
	@Autowired
	private ProducerQueue producerQueue;
	
	/**
	 * 抢购接口
	 * 
	 * @param inventoryId 库存id
	 * @param phone 手机号
	 */
	@RequestMapping("subtractInventory")
	public String add(Integer inventoryId, String phone){
		/*
		// 1.限制相同用户5秒内只能请求一次
		Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(phone, inventoryId, 5000, TimeUnit.MILLISECONDS);
		if(!setIfAbsent){
			return "抢购频繁请稍后...";
		}
		*/
		
		// 2. 从redis获取令牌,能获取到数据就代表抢到了商品从而执行以下流程,否则返回售无。
		Object leftPop = redisTemplate.opsForList().leftPop(inventoryId+"");
		if(leftPop == null){
			log.info("未抢到{}。", inventoryId);
			return "商品已售无...";
		}
		
		// 3.投递到mq(走到这一步的用户已经抢购成功,只不过交给mq异步进行后续处理)
		JSONObject jSONObject = new JSONObject();
		jSONObject.put("phone", phone);
		jSONObject.put("inventoryId", inventoryId);
		producerQueue.purchaseQueue(jSONObject);
		
		return "正在抢购中...";
	}
	
	/**
	 * 前台ajax实时查询用户是否抢购成功
	 * 
	 * @param inventoryId 库存id
	 * @param phone 手机号
	 * @return
	 */
	@RequestMapping("getInventoryLog")
	public String getInventoryLog(int inventoryId, String phone){
		// 1. 查询用户抢购成功记录表,也可改为实时查询redis
		Integer id = mapper.getInventoryLog(inventoryId, phone);
		if(id != null){
			return phone + "用户,抢购成功!";
		}
		
		// 2. 查询redis是否还有库存令牌
		Long size = redisTemplate.opsForList().size(inventoryId+"");
		if(size == null || size == 0){
			return "商品已售无!";
		}
		
		return null;
	}

	/**
	 * 提前在后台管理中把库存数量放入redis中,采用list命令(特性从左获取并移除)
	 * @param inventoryId 库存id
	 * @param quantity 库存数量
	 * @return
	 */
	@RequestMapping("addInventoryToRedis")
	public String addInventoryToRedis(Integer inventoryId,int quantity){
		for (int i = 1; i <= quantity; i++) {
			redisTemplate.opsForList().leftPush(inventoryId+"", i); // i变量也可改为uuid令牌,只不过此版本后续操作没用到令牌
		}
		return "生成redis库存数量:" + quantity;
	}
	
}

2. mapper

package com.chuangqi.defense.mapper;

import java.util.Date;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

public interface Test_01Mapper {
    
	// 行锁
	@Update("update inventory set quantity = quantity - 1 where inventoryId = #{inventoryId} and quantity > 0")
	Integer subtractInventory(@Param("inventoryId")int inventoryId);

	// 乐观锁
	@Update("UPDATE inventory SET quantity = quantity - 1, version = version + 1 WHERE inventoryId = #{inventoryId} AND quantity > 0 AND version = #{version}")
	Integer subtractInventoryOptimisticByVersion(@Param("inventoryId")int inventoryId, @Param("version")Integer version);
	
	// 查询商品库存的版本号(用于乐观锁)
	@Select("select version from inventory where inventoryId = #{inventoryId} limit 1")
	Integer getInventory(@Param("inventoryId")Integer inventoryId);
	
	// 抢购成功,添加用户到抢购成功记录表
	@Insert("insert into inventory_log (phone, inventoryId, dateTime) values(#{phone},#{inventoryId},#{date})")
	Integer addInventoryLog(@Param("inventoryId")int inventoryId, @Param("phone")String phone, @Param("date")Date date);

	// 查询用户是否抢购成功
	@Select("select id from inventory_log where phone = #{phone} and inventoryId = #{inventoryId} limit 1")
	Integer getInventoryLog(@Param("inventoryId")int inventoryId, @Param("phone")String phone);

}

3. mq配置

3.1. rabbitmq配置类

package com.chuangqi.defense.mq;

import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * rabbitmq配置类
 *
 * @author qizhentao
 * @version V1.0
 */
@Configuration
public class RabbitConf {
	
	// 队列名称
	private final String purchase_queue = "purchase_queue";
	
	/**
	 * 创建一个队列
	 * @return
	 */
	@Bean
    public Queue purchase() {
         return new Queue(purchase_queue);
    }
   
}

3.2. 生产者

package com.chuangqi.defense.mq;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import cn.hutool.json.JSONObject;

/**
 * 生产者
 *
 * @author qizhentao
 * @version V1.0
 */
@Component
public class ProducerQueue {
	
    @Autowired
    private AmqpTemplate template;

    public void purchaseQueue(JSONObject info) {
    	// 投递消息到名为purchase_queue的队列中
    	template.convertAndSend("purchase_queue", info);
    }
    
}

3.3. 消费者

package com.chuangqi.defense.mq;

import java.util.Date;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.chuangqi.defense.mapper.Test_01Mapper;
import cn.hutool.json.JSONObject;
import lombok.extern.log4j.Log4j2;

/**
 * 消费者监听queue队列
 *
 * @author qizhentao
 * @version V1.0
 */
@Log4j2
@Component
public class ConsumerQueue {
	
	@Autowired
	private Test_01Mapper mapper;

    @RabbitListener(queues = "purchase_queue")    //监听器监听指定的Queue(purchase_queue)
    public void processA(JSONObject info) {
    	log.info("消费者收到消息:{}", info);
    	// 库存id
    	int inventoryId = Integer.parseInt(info.get("inventoryId").toString());
    	// 手机号
    	String phone = info.get("phone").toString();
    	
		// 3. 基于行锁减库存
		// Integer i = mapper.subtractInventory(inventoryId);
    	
		// 3. 基于乐观锁减库存
    	Integer version = mapper.getInventory(inventoryId);// 查询商品库存是否存在 ------ 使用redis+mq了就没必要使用乐观锁了,直接使用行锁就行  
		Integer i = mapper.subtractInventoryOptimisticByVersion(inventoryId, version);
		if(i == 1){
			// 4. 异步添加用户到抢购成功记录表
			addInventoryLog(inventoryId, phone);
		}
    }
    
    /**
     * 异步添加用户到抢购成功记录表 --- 也可添加线程池使用多线程异步执行!!!
     * @param inventoryId 商品库存id
     * @param phone 手机号
     * @return
     */
 	@Async
 	public boolean addInventoryLog(int inventoryId, String phone){
 		int i = mapper.addInventoryLog(inventoryId, phone, new Date());
 		// 也可添加到redis,前台ajax实时查询redis即可
 		// redisTemplate.opsForValue().set(phone, inventoryId);
 		return i == 1 ? true : false;
 	}

}

4. 表结构

SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `inventory`;
CREATE TABLE `inventory` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `inventoryId` int(11) DEFAULT NULL COMMENT '库存id',
  `quantity` int(11) DEFAULT NULL COMMENT '库存数量',
  `version` int(11) DEFAULT '0' COMMENT '基于版本-乐观锁',
  `updateTime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deadline` datetime DEFAULT NULL COMMENT '截止时间',
  `insertTime` datetime DEFAULT NULL COMMENT '新增时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='库存表';


SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `inventory_log`;
CREATE TABLE `inventory_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `phone` varchar(255) DEFAULT NULL COMMENT '用户手机号',
  `inventoryId` int(11) DEFAULT NULL COMMENT '库存id',
  `dateTime` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=36119 DEFAULT CHARSET=utf8 COMMENT='用户抢购成功记录表';

5. 测试

5.1. 给商品库存id为2的添加100条库存

5.2. 添加库存数量到redis

5.3. jmeter多线程模拟抢购

5.4. jmeter执行结束后查看库存的变化

  5.4.1. 库存为0

  5.4.2.用户抢购成功记录表为100条

5.5. 查询是否抢购成功 --(前台ajax定时实时查询此接口)


6. 生产者和消费者要分两个服务:


Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐