秒杀系统,系统瞬间要处理大量并发,核心问题在于如何在大并发的情况下能保证 DB能扛得住压力,因为高并发的瓶颈就在于DB。如果说请求直接从前端透传到DB,显然,DB是无法承受几十万上百万甚至上千万的并发量的,这里就用到了另外一个非常重要的组件:消息队列。我们不是把请求直接去访问数据库,而是先把请求写到消息队列中,做一个缓存,然后再去慢慢的更新数据库。
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相关技术文章或行业资讯
欢迎大家关注和转发文章,后期还有福利赠送!