实战高并发秒杀实现(3):基于Token令牌桶+MQ实现修改库存
程序员文章站
2022-03-28 11:06:31
...
一、理论基础
1.1、前端优化
- 使用动静分离、将静态资源存放到第三方文件服务器中实现cdn加速,目的减轻秒杀抢购带宽
- 当用户点击秒杀按钮的时候,应该将按钮disabled 防止重复提交
- 使用复杂的图形验证码防止机器模拟
- 秒杀详情页面,使用定时器根据用户信息查询秒杀结果
- 商品的详情页面使用nginx+lua+openresty 实现静态化页面
1.2、 网关
- ratelimter、nginx、hystrix、redis实现限流 令牌痛+漏铜算法 对用户秒杀请求实现限流和服务保护。
- 用户黑名单和白名单拦截
1.3、 秒杀接口
- 服务降级级、隔离、熔断
- 从redis中获取秒杀的令牌(能够获取到令牌就能够秒杀成功,否则就秒杀失败!)
- 异步使用MQ执行修改库存操作
- 提供一个根据用户信息查询秒杀结果接口
1.4、 项目部署
- Nginx+lvs 实现服务高可用和集群
- 分时段抢购(12306在用,中午 下午 )
1.5、同时有10万个请求实现秒杀、商品库存只有100个,要实现只需要修改库存100次
方案实现流程:
提前对应的商品库存生成好对应令牌(100个令牌)——>也就是令牌桶,在10万个请求中,只要谁能够获取到令牌谁就能够秒杀成功, 获取到秒杀令牌后,在使用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("恭喜你秒杀成功!");
}
}
上一篇: 法庭一般是这样审案的。
下一篇: 如何理解及安装Nginx