欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

实战高并发秒杀实现(3):基于Token令牌桶+MQ实现修改库存

程序员文章站 2022-03-28 11:06:31
...

一、理论基础

1.1、前端优化

  1. 使用动静分离、将静态资源存放到第三方文件服务器中实现cdn加速,目的减轻秒杀抢购带宽
  2. 当用户点击秒杀按钮的时候,应该将按钮disabled  防止重复提交
  3. 使用复杂的图形验证码防止机器模拟
  4. 秒杀详情页面,使用定时器根据用户信息查询秒杀结果
  5. 商品的详情页面使用nginx+lua+openresty 实现静态化页面

1.2、 网关

  1. ratelimter、nginx、hystrix、redis实现限流 令牌痛+漏铜算法  对用户秒杀请求实现限流服务保护
  2. 用户黑名单和白名单拦截

1.3、 秒杀接口

  1. 服务降级级、隔离、熔断
  2. 从redis中获取秒杀的令牌(能够获取到令牌就能够秒杀成功,否则就秒杀失败!)
  3. 异步使用MQ执行修改库存操作
  4. 提供一个根据用户信息查询秒杀结果接口

1.4、 项目部署

  1. Nginx+lvs 实现服务高可用和集群
  2. 分时段抢购(12306在用,中午 下午

1.5、同时有10万个请求实现秒杀、商品库存只有100个,要实现只需要修改库存100

方案实现流程:

提前对应的商品库存生成好对应令牌(100个令牌)——>也就是令牌桶,在10万个请求中,只要谁能够获取到令牌谁就能够秒杀成功, 获取到秒杀令牌后,在使用mq异步实现修改减去库

 

二、代码实现

原理图:

注意:公司级秒杀服务中,秒杀生产者和消费者要分别放在不同的服务,避免生产者挂了消费者也连着挂了

实战高并发秒杀实现(3):基于Token令牌桶+MQ实现修改库存

2.1、生产者

(1)MQ相关配置:RabbitmqConfig

@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)生产者发送消息:SpikeCommodityProducer

@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、消费者

StockConsumer

@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);
			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);
	}

	// 调用数据库层判断
	public Boolean toDaoResult(int result) {
		return result > 0 ? true : false;
	}

}

 

 

2.3、根据手机号码和商品库存id查询秒杀记录

(1)OrderSeckillService :

public interface OrderSeckillService {
	@RequestMapping("/getOrder")
	public BaseResponse<JSONObject> getOrder(String phone, Long seckillId);

}

(2)OrderSeckillServiceImpl 

@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("恭喜你秒杀成功!");
	}

}

 

相关标签: 项目实战