淘東電商項目(76) -秒殺系統(完整代碼實現)

引言

本文代碼已提交至Github(版本號:2c985822b282756e3fd70490cb0ba6f4f2140e47),有興趣的同學可以下載來看看:https://github.com/ylw-github/taodong-shop

秒殺系統在前面已經講解了“前端優化”以及“防止庫存超賣”的功能,但是在效率這一塊還是很慢的,那麼後臺的秒殺完整代碼流程是如何的呢?本文來講解下,閱讀前,童鞋們可以先閱讀之前寫的博客:

本文目錄結構:
l____引言
l____ 1.秒殺原理圖
l____ 2. 後臺核心代碼
l________ 2.1 令牌桶生成接口
l________ 2.2 秒殺接口(核心)
l________________ 2.2.1 MQ配置
l________________ 2.2.2 生產者
l________________ 2.2.3 消費者
l________ 2.3 用戶查詢接口
l____ 3. 測試

1.秒殺原理圖

下面貼上我自己整理的原理圖,如下:

在這裏插入圖片描述
從原理圖,可以看到秒殺的流程大致如下:

  1. 商戶添加秒殺商品的時候,後臺會自動從Redis裏生成令牌桶,如商品A的庫存有100個,那麼當用戶修改商品時會去Redis裏添加一條數據,格式:商品id+List令牌桶(數量是庫存數量)
  2. 用戶搶購時,會從令牌桶裏獲取令牌,如果能獲取成功,則通過MQ去異步修改數據庫裏面的訂單表以及秒殺表。
  3. 搶購完成後,會提示用戶“正在排隊中…”,用戶需要自己主動的去查詢搶購結果。

2. 後臺核心代碼

2.1 令牌桶生成接口

令牌桶生成接口核心代碼:

@Override
public BaseResponse<JSONObject> addSpikeToken(Long seckillId, Long tokenQuantity) {
	// 1.驗證參數
	if (seckillId == null) {
		return setResultError("商品庫存id不能爲空!");
	}
	if (tokenQuantity == null) {
		return setResultError("token數量不能爲空!");
	}
	SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
	if (seckillEntity == null) {
		return setResultError("商品信息不存在!");
	}
	// 2.使用多線程異步生產令牌
	createSeckillToken(seckillId, tokenQuantity);
	return setResultSuccess("令牌正在生成中.....");
}

@Async
public void createSeckillToken(Long seckillId, Long tokenQuantity) {
	generateToken.createListToken("seckill_", seckillId + "", tokenQuantity);
}

Redis令牌桶生成工具類:

①GenerateToken

public void createListToken(String keyPrefix, String redisKey, Long tokenQuantity) {
    List<String> listToken = getListToken(keyPrefix, tokenQuantity);
    redisUtil.setList(redisKey, listToken);
}

public List<String> getListToken(String keyPrefix, Long tokenQuantity) {
    List<String> listToken = new ArrayList<>();
    for (int i = 0; i < tokenQuantity; i++) {
        String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
        listToken.add(token);
    }
    return listToken;

}

②RedisUtil:

public void setList(String key, List<String> listToken) {
	stringRedisTemplate.opsForList().leftPushAll(key, listToken);
}

2.2 秒殺接口(核心)

2.2.1 MQ配置

①application.yml配置:

rabbitmq:
    ####連接地址
    host: 127.0.0.1
    ####端口號   
    port: 5672
    ####賬號 
    username: guest
    ####密碼  
    password: guest
    ### 地址
    virtual-host: spike_host
    listener:
      simple:
        retry:
          ####開啓消費者(程序出現異常的情況下會)進行重試
          enabled: true
          ####最大重試次數
          max-attempts: 5
          ####重試間隔時間
          initial-interval: 1000
        ####開啓手動ack  
        acknowledge-mode: manual
        default-requeue-rejected: false

②RabbitMQ配置:

/**
 * description: RabbitmqConfig 配置
 * create by: YangLinWei
 * create time: 2020/5/26 10:54 上午
 */
@Component
public class RabbitmqConfig {

	// 添加修改庫存隊列
	public static final String MODIFY_INVENTORY_QUEUE = "modify_inventory_queue";
	// 交換機名稱
	private static final String MODIFY_EXCHANGE_NAME = "modify_exchange_name";

	// 1.添加交換機隊列
	@Bean
	public Queue directModifyInventoryQueue() {
		return new Queue(MODIFY_INVENTORY_QUEUE);
	}

	// 2.定義交換機
	@Bean
	DirectExchange directModifyExchange() {
		return new DirectExchange(MODIFY_EXCHANGE_NAME);
	}

	// 3.修改庫存隊列綁定交換機
	@Bean
	Binding bindingExchangeintegralDicQueue() {
		return BindingBuilder.bind(directModifyInventoryQueue()).to(directModifyExchange()).with("modifyRoutingKey");
	}

}

2.2.2 生產者

/**
 * description: 秒殺生產者
 * create by: YangLinWei
 * create time: 2020/5/26 10:58 上午
 */
@Component
@Slf4j
public class SpikeCommodityProducer implements RabbitTemplate.ConfirmCallback {

	@Autowired
	private RabbitTemplate rabbitTemplate;

	@Transactional
	public void send(JSONObject jsonObject) {

		String jsonString = jsonObject.toJSONString();
		System.out.println("jsonString:" + jsonString);
		String messAgeId = UUID.randomUUID().toString().replace("-", "");
		// 封裝消息
		Message message = MessageBuilder.withBody(jsonString.getBytes())
				.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8").setMessageId(messAgeId)
				.build();
		// 構建回調返回的數據(消息id)
		this.rabbitTemplate.setMandatory(true);
		this.rabbitTemplate.setConfirmCallback(this);
		CorrelationData correlationData = new CorrelationData(jsonString);
		rabbitTemplate.convertAndSend("modify_exchange_name", "modifyRoutingKey", message, correlationData);

	}

	// 生產消息確認機制 生產者往服務器端發送消息的時候,採用應答機制
	@Override
	public void confirm(CorrelationData correlationData, boolean ack, String cause) {
		String jsonString = correlationData.getId();
		System.out.println("消息id:" + correlationData.getId());
		if (ack) {
			log.info(">>>使用MQ消息確認機制確保消息一定要投遞到MQ中成功");
			return;
		}
		JSONObject jsonObject = JSONObject.parseObject(jsonString);
		// 生產者消息投遞失敗的話,採用遞歸重試機制
		send(jsonObject);
		log.info(">>>使用MQ消息確認機制投遞到MQ中失敗");
	}
}

2.2.3 消費者

/**
 * description: 庫存消費者
 * create by: YangLinWei
 * create time: 2020/5/26 10:59 上午
 */
@Component
@Slf4j
public class StockConsumer {
    @Autowired
    private SeckillMapper seckillMapper;
    @Autowired
    private OrderMapper orderMapper;

    @RabbitListener(queues = "modify_inventory_queue")
    @Transactional
    public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(), "UTF-8");
        log.info(">>>messageId:{},msg:{}", messageId, msg);
        JSONObject jsonObject = JSONObject.parseObject(msg);
        // 1.獲取秒殺id
        Long seckillId = jsonObject.getLong("seckillId");
        SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
        if (seckillEntity == null) {
            log.warn("seckillId:{},商品信息不存在!", seckillId);
            basicNack(message, channel);
            return;
        }
        Long version = seckillEntity.getVersion();
        int inventoryDeduction = seckillMapper.optimisticDeduction(seckillId, version);
        if (!toDaoResult(inventoryDeduction)) {
            log.info(">>>seckillId:{}修改庫存失敗>>>>inventoryDeduction返回爲{} 秒殺失敗!", seckillId, inventoryDeduction);
            basicNack(message, channel);
            return;
        }
        // 2.添加秒殺訂單
        OrderEntity orderEntity = new OrderEntity();
        String phone = jsonObject.getString("phone");
        orderEntity.setUserPhone(phone);
        orderEntity.setSeckillId(seckillId);
        orderEntity.setState(1l);
        int insertOrder = orderMapper.insertOrder(orderEntity);
        if (!toDaoResult(insertOrder)) {
            basicNack(message, channel);
            return;
        }
        log.info(">>>修改庫存成功seckillId:{}>>>>inventoryDeduction返回爲{} 秒殺成功", seckillId, inventoryDeduction);
        basicNack(message, channel);
    }

    // 調用數據庫層判斷
    public Boolean toDaoResult(int result) {
        return result > 0 ? true : false;
    }

    // 消費者獲取到消息之後 手動簽收 通知MQ刪除該消息
    private void basicNack(Message message, Channel channel) throws IOException {
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
    }
}

2.3 用戶查詢接口

@RestController
public class OrderSeckillServiceImpl extends BaseApiService<JSONObject> implements OrderSeckillService {
	@Autowired
	private OrderMapper orderMapper;

	@Override
	public BaseResponse<JSONObject> getOrder(String phone, Long seckillId) {
		if (StringUtils.isEmpty(phone)) {
			return setResultError("手機號碼不能爲空!");
		}
		if (seckillId == null) {
			return setResultError("商品庫存id不能爲空!");
		}
		OrderEntity orderEntity = orderMapper.findByOrder(phone, seckillId);
		if (orderEntity == null) {
			return setResultError("正在排隊中.....");
		}
		return setResultSuccess("恭喜你秒殺成功!");
	}
}

3. 測試

①模擬用戶修改商品庫存,更新令牌桶,瀏覽器訪問:http://localhost:9800/addSpikeToken?seckillId=100001&tokenQuantity=100
在這裏插入圖片描述
可以看到Redis裏生成商品key id爲100001,值爲list,大小爲100的集合:
在這裏插入圖片描述
②模擬搶購,瀏覽器訪問:http://localhost:9800/spike?phone=13800000001&seckillId=100001
在這裏插入圖片描述
可以看到數據庫庫存減一:
在這裏插入圖片描述
訂單並生成了一條記錄:
在這裏插入圖片描述
Redis減少了一個令牌:
在這裏插入圖片描述
③模擬用戶查詢搶購結果,瀏覽器訪問:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章