分析java高并发秒杀系统(后端)实现思路

秒杀系统实现思路

秒杀系统,系统瞬间要处理大量并发,核心问题在于如何在大并发的情况下能保证 DB能扛得住压力,因为高并发的瓶颈就在于DB。如果说请求直接从前端透传到DB,显然,DB是无法承受几十万上百万甚至上千万的并发量的,这里就用到了另外一个非常重要的组件:消息队列。我们不是把请求直接去访问数据库,而是先把请求写到消息队列中,做一个缓存,然后再去慢慢的更新数据库。

思路

  1. 系统初始化,把商品库存数量加载到Redis上面来。
  2. 后端收到秒杀请求,Redis预减库存,如果库存已经到达临界值的时候,就不需要继续请求下去,直接返回失败,即后面的大量请求无需给系统带来压力。
  3. 判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品,判断是否重复秒杀。
  4. 库存充足,且无重复秒杀,将秒杀请求封装后消息入队,同时给前端返回一个code (0),即代表返回排队中。(返回的并不是失败或者成功,此时还不能判断)前端接收到数据后,显示排队中,并根据商品id轮询请求服务器(考虑200ms轮询一次)。
  5. 后端RabbitMQ监听秒杀的订单信息,获取到传入的信息,执行真正的秒杀之前,要判断数据库的库存,判断是否重复秒杀,然后执行秒杀事务(秒杀事务是一个原子操作:库存减1,下订单,写入秒杀订单)。

代码实现

1.将要秒杀的商品生成对应商品数量token存储到 redis,减轻数据库压力

/**
	 * 采用redis数据库类型为 list类型 key为 商品库存id list 多个秒杀token
	 * 
	 * @param seckillId  商品id
	 * @param tokenQuantity 令牌数量,对应商品数量
	 * @return
	 */
	// 采用redis数据库类型为 list类型 key为 商品库存id list 多个秒杀token
	@RequestMapping("/addSpikeToken")
	public BaseResponse 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("令牌正在生成中.....");
	}

这里采用异步多线程生成token

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

生成token并存入redis


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

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

	}

2.用户访问秒杀接口时,会先去redis中获取对应商品的token

/**
	 * 注解 AOP 减少代码重复调用 使用网关开启限流
	 */
	/**
	 * 用户秒杀接口 phone和userid都可以的
	 *
	 * @phone 手机号码
* @seckillId 要秒杀的商品id * @return */ @RequestMapping("/spike") @Transactional(rollbackFor = Exception.class) public BaseResponse spike(String phone, Long seckillId) { log.info("###>>>>>秒杀接口线程池名称:" + Thread.currentThread().getName()); // 1.参数验证 if (StringUtils.isEmpty(phone)) { return setResultError("手机号码不能为空!"); } if (seckillId == null) { return setResultError("商品库存id不能为空!"); } // 2.从redis从获取对应的秒杀token String seckillToken = generateToken.getListKeyToken(seckillId + ""); if (StringUtils.isEmpty(seckillToken)) { log.info(">>>seckillId:{}, 亲,该秒杀已经售空,请下次再来!", seckillId); return setResultError("亲,该秒杀已经售空,请下次再来!"); } // 3.获取到秒杀token之后,异步放入mq中实现修改商品的库存 sendSeckillMsg(seckillId, phone); return setResultSuccess("正在排队中......."); }

这里使用leftPop取出一个token,同时redis中的token就少了一个

public String getListKeyToken(String key) {

	String value = redisUtil.getStringRedisTemplate().opsForList().leftPop(key);
	return value;
}

这里同样采用异步多线程方式发送到rabbitmq消息对队列

/**
	 * 获取到秒杀token之后,异步放入mq中实现修改商品的库存
	 */
	@Async
	public void sendSeckillMsg(Long seckillId, String phone) {
		JSONObject jsonObject = new JSONObject();
		jsonObject.put("seckillId", seckillId);
		jsonObject.put("phone", phone);
		spikeCommodityProducer.send(jsonObject);
	}
@Transactional(rollbackFor = Exception.class)
	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);

		//发送消息到rabbitmq
		rabbitTemplate.convertAndSend("modify_exchange_name", "modifyRoutingKey", message, correlationData);

	}

3.订单系统会监听rabbitmq 的消息,进行消费

/**
	 * 订单服务监听修改库存的队列
	 *
	 * @param message  队列中存储的信息
	 * @param headers
	 * @param channel
	 * @throws IOException
	 */
	@RabbitListener(queues = "modify_inventory_queue")
	@Transactional(rollbackFor = Exception.class)
	public void process(Message message, @Headers Map 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);
			return;
		}
		Long version = seckillEntity.getVersion();
		// 跟新库存信息
		int inventoryDeduction = seckillMapper.inventoryDeduction(seckillId, version);
		if (!toDaoResult(inventoryDeduction)) {
			log.info(">>>seckillId:{}修改库存失败>>>>inventoryDeduction返回为{} 秒杀失败!", seckillId, inventoryDeduction);
			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)) {
			return;
		}
		log.info(">>>修改库存成功seckillId:{}>>>>inventoryDeduction返回为{} 秒杀成功", seckillId, inventoryDeduction);
	}

感谢你看到这里,我是程序员麦冬,一个java开发从业者,深耕行业六年了,每天都会分享java相关技术文章或行业资讯

欢迎大家关注和转发文章,后期还有福利赠送!

你可能感兴趣的:(技术文,队列,java,redis,分布式,多线程)