秒杀的实现
一、数据库
秒杀商品表:
秒杀时间表:
二、流程:
1、添加秒杀商品:先查看是否有该时间段,没有就添加一个时间段,并把商品信息添加到秒杀商品表里。
2、获取当前时间及往后7个时间段, 总共8个。
3、根据时间段id获取商品
4、根据秒杀id获取商品详细信息
1 从缓存中查询
2. 缓存没有,再从数据库中获取,添加到缓存
3. 将库存单独存入一个key中
4. 返回商品
5、秒杀开始
1 判断是否卖完
2. 判断是否秒杀开始,虽然前端在没开始之前不会打开抢购按钮,但为了防止路径暴露被刷
3. 判断是否已经秒杀到了,避免一个账户秒杀多个商品
4. 预减库存:从缓存中减去库存、减少访问数据库次数
5. 判断是否已经卖完,设置内存标记
6. 使用RabbitMQ异步传输
6、 使用RabbitMQ接收信息进行处理
1 判断是否消费过了
2. 将消费信息存入redis
3. 添加订单,将用户信息存入redis中的已消费集合中。
4. 如果发生错误,删除消费信息、将消息放入队列尾部,尝试再次消费
三、代码
秒杀代码
@Transactional
public void seckillProduct(String seckillId, Integer userId) {
if (localOverMap.get(seckillId) != null && localOverMap.get(seckillId)) {
// 售空
throw new XmException(ExceptionEnum.GET_SECKILL_IS_OVER);
}
// 判断秒杀是否开始, 防止路径暴露被刷
Map m = redisTemplate.opsForHash().entries(RedisKey.SECKILL_PRODUCT + seckillId);
if (!m.isEmpty()) {
SeckillProductVo seckillProductVo = null;
try {
seckillProductVo = BeanUtil.map2bean(m, SeckillProductVo.class);
} catch (Exception e) {
e.printStackTrace();
}
// 秒杀开始时间
Long startTime = seckillProductVo.getStartTime();
if (startTime >System.currentTimeMillis()) {
throw new XmException(ExceptionEnum.GET_SECKILL_IS_NOT_START);
}
}
// 判断是否已经秒杀到了,避免一个账户秒杀多个商品
List<String> list = redisTemplate.opsForList().range(RedisKey.SECKILL_PRODUCT_USER_LIST + seckillId, 0, -1);
if (list.contains(String.valueOf(userId))) {
throw new XmException(ExceptionEnum.GET_SECKILL_IS_REUSE);
}
// 预减库存:从缓存中减去库存
// 利用redis中的方法,减去库存,返回值为减去1之后的值
if (stringRedisTemplate.opsForValue().decrement(RedisKey.SECKILL_PRODUCT_STOCK + seckillId) < 0) {
// 设置内存标记
localOverMap.put(seckillId, true);
// 秒杀完成,库存为空
throw new XmException(ExceptionEnum.GET_SECKILL_IS_OVER);
}
// 使用RabbitMQ异步传输
mqSend(seckillId, userId);
}
发送消息
private void mqSend(String seckillId, Integer userId) {
HashMap<String, String> map = new HashMap<>();
map.put("seckillId", seckillId);
map.put("userId", userId.toString());
// 设置ID,保证消息队列幂等性
CorrelationData correlationData = new CorrelationData();
correlationData.setId(seckillId + ":" + userId);
try {
rabbitTemplate.convertAndSend("seckill_order", map, correlationData);
} catch (AmqpException e) {
// 发送消息失败
e.printStackTrace();
stringRedisTemplate.opsForValue().increment(RedisKey.SECKILL_PRODUCT_STOCK + seckillId);
}
}
接收消息
@RabbitListener(queues = "seckill_order")
public void insertOrder(Map map, Channel channel, Message message){
String seckillId = (String) map.get("seckillId");
String userId = (String) map.get("userId");
// 查看id,保证幂等性
String correlationId = seckillId + ":" + userId;
if (stringRedisTemplate.hasKey(RedisKey.SECKILL_RABBITMQ_ID + correlationId)) {
System.out.println(stringRedisTemplate.hasKey(RedisKey.SECKILL_RABBITMQ_ID + correlationId));
// redis中存在,表明此条消息已消费,请勿重复消费
return;
}
// 存入redis,因为只需要判断是否存在,因此value为多少无所谓
stringRedisTemplate.opsForValue().set(RedisKey.SECKILL_RABBITMQ_ID + correlationId, "1");
Long seckillEndTime = seckillProductService.getEndTime(seckillId);
stringRedisTemplate.expire(RedisKey.SECKILL_RABBITMQ_ID + correlationId, seckillEndTime - new Date().getTime(), TimeUnit.SECONDS); // 设置过期时间
try {
//添加订单
orderService.addSeckillOrder(seckillId, userId);
} catch (Exception e) {
e.printStackTrace();
try {
stringRedisTemplate.delete(RedisKey.SECKILL_RABBITMQ_ID + correlationId);
// 将该消息放入队列尾部,尝试再次消费
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}