秒杀系统实现思路
秒杀系统,系统瞬间要处理大量并发,核心问题在于如何在大并发的情况下能保证 DB 能扛得住压力,因为高并发的瓶颈就在于DB。如果说请求直接从前端透传到 DB,显然,DB是无法承受几十万上百万甚至上千万的并发量的,这里就用到了另外一个非常重要的组件:消息队列。我们不是把请求直接去访问数据库,而是先把请求写到消息队列中,做一个缓存,然后再去慢慢的更新数据库。
思路
- 系统初始化,把商品库存数量加载到Redis上面来。
- 后端收到秒杀请求,Redis预减库存,如果库存已经到达临界值的时候,就不需要继续请求下去,直接返回失败,即后面的大量请求无需给系统带来压力。
- 判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品,判断是否重复秒杀。
- 库存充足,且无重复秒杀,将秒杀请求封装后消息入队,同时给前端返回一个code
(0),即代表返回排队中。(返回的并不是失败或者成功,此时还不能判断)
前端接收到数据后,显示排队中,并根据商品id轮询请求服务器(考虑200ms轮询一次)。 - 后端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<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("令牌正在生成中.....");
}
这里采用异步多线程生成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<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;
}
2.用户访问秒杀接口时,会先去redis中获取对应商品的token,
/**
* 注解 AOP 减少代码重复调用 使用网关开启限流
*/
/**
* 用户秒杀接口 phone和userid都可以的
*
* @phone 手机号码<br>
* @seckillId 要秒杀的商品id
* @return
*/
@RequestMapping("/spike")
@Transactional(rollbackFor = Exception.class)
public BaseResponse<JSONObject> 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<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);
}