秒杀业务场景的处理方案

秒杀的处理方案

秒杀技术实现核心思想是运用缓存减少数据库瞬间的访问压力。在秒杀时,首先会将数据库的秒杀商品同步到缓存中,用户从缓存中查询秒杀商品,抢购商品时减少缓存中的库存数量。产生的秒杀订单先写到缓存,付款成功后再写入数据库。

同步秒杀商品到redis

我们需要将正在秒杀的商品从数据库同步保存到redis中,在redis中

秒杀商品是以Hash类型保存,Hash的键是商品id,值是商品对象。

用户只能查询正在秒杀的商品 ( 开始时间 < 当前时间 < 结束时间,且库存 > 0 ) ,所以我们在redis中只保存正在秒杀的商品。由于每分钟都有商品开始秒杀,也有商品结束秒杀。所以需要定时查询数据库中正在秒杀的商品,同步到redis中。我们使用SpringTask技术,每分钟同步一次数据

      用户秒杀会修改redis中的商品库存,而此时mysql中的库存是没有修改的。等到下次同步数据的时候,redis中的库存数就又成mysql中没有修改过的库存了。为了保证数据的同步,我们在将数据库数据同步到redis之前,先将redis中的商品库存数据同步到数据库中。

定时任务同步redis和数据库可参考示例代码:

/**
     * 每分钟查询一次数据库,更新redis中的秒杀商品数据
     * 条件为startTime < 当前时间 < endTime,库存大于0
     */
    @Scheduled(cron = "0 * * * * *")
    public void refreshRedis() {
        // 将redis中秒杀商品的库存数据同步到mysql
        List<SeckillGoods> seckillGoodsListOld = redisTemplate.boundHashOps("seckillGoods").values();
        for (SeckillGoods seckillGoods : seckillGoodsListOld) {
            // 在数据库中查询秒杀商品
            SeckillGoods sqlSeckillGoods = seckillGoodsMapper.selectById(seckillGoods.getId());
            // 修改秒杀商品的库存
            sqlSeckillGoods.setStockCount(seckillGoods.getStockCount());
            seckillGoodsMapper.updateById(sqlSeckillGoods);
        }
        
        // 1.查询数据库中正在秒杀的商品
        QueryWrapper<SeckillGoods> queryWrapper = new QueryWrapper();
        Date date = new Date();
        String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
        queryWrapper.le("startTime", now) // 当前时间晚于开始时间
                .ge("endTime", now) // 当前时间早于开始时间
                .gt("stockCount", 0); // 库存大于0
        List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectList(queryWrapper);

        // 2.删除之前的秒杀商品
        redisTemplate.delete("seckillGoods");

        // 3.保存现在正在秒杀的商品
        for (SeckillGoods seckillGoods : seckillGoodsList) {
            redisTemplate.boundHashOps("seckillGoods").put(seckillGoods.getGoodsId(), seckillGoods);
        }
    }

分页查询秒杀商品列表(返回有分页的格式)

将redis中存储的秒杀商品数据构造分页结构返回给前端可参考如下代码:

@Override
    public Page<SeckillGoods> findPageByRedis(int page, int size) {
        // 1. 查询所有秒杀商品
        List<SeckillGoods> seckillGoodsList = redisTemplate.boundHashOps("seckillGoods").values();

        // 2. 获取当前页商品列表
        // 开始截取索引
        int start = (page - 1) * size;
        // 结束截取索引
        int end = start + size > seckillGoodsList.size() ? seckillGoodsList.size():start + size;
        // 获取当前页结果集
        List<SeckillGoods> seckillGoods = seckillGoodsList.subList(start, end);

        // 3. 构造页面对象
        Page<SeckillGoods> seckillGoodsPage = new Page();
        seckillGoodsPage.setCurrent(page) // 当前页
                        .setSize(size) // 每页条数
                        .setTotal(seckillGoodsList.size()) // 总条数
                        .setRecords(seckillGoods); //结果集
        return seckillGoodsPage;
    }

根据id查询秒杀商品

 @Override
    public SeckillGoods findSeckillGoodsByRedis(Long goodsId) {
        return (SeckillGoods) redisTemplate.boundHashOps("seckillGoods").get(goodsId);
    }

生成秒杀订单

为了让用户购买速度更快,秒杀商品时不会将商品添加到购物车,而是直接生成订单。并且由于访问量较大,为了避免数据库压力过大,我们会先将订单数据保存在redis当中,等用户支付完成后,再将redis中的订单数据保存到数据库中。

在用户成功秒杀下单后,商品库存减少,如果用户长时间不支付,则该商品始终被用户占据,其他用户也无法购买。我们需要给订单设置过期时间,过期后删除订单,回退商品库存。

创建订单简单示例代码

@Override
    public Orders createOrder(Orders orders) {
        // 1.生成订单对象
        orders.setId(IdWorker.getIdStr()); // 手动生产订单id
        orders.setStatus(1); // 订单状态未付款
        orders.setCreateTime(new Date()); // 订单创建时间
        orders.setExpire(new Date(new Date().getTime()+1000*60*5));
        // 计算商品价格
        CartGoods cartGoods = orders.getCartGoods().get(0);
        Integer num = cartGoods.getNum();
        BigDecimal price = cartGoods.getPrice();
        BigDecimal sum = price.multiply(BigDecimal.valueOf(num));
        orders.setPayment(sum);

        // 2.减少秒杀商品库存
        // 查询秒杀商品
        SeckillGoods seckillGoods = findSeckillGoodsByRedis(cartGoods.getGoodId());
        // 查询库存,库存不足抛出异常
        Integer stockCount = seckillGoods.getStockCount();
        if (stockCount <= 0){
            throw new BusException(CodeEnum.NO_STOCK_ERROR);
        }
        // 减少库存
        seckillGoods.setStockCount(seckillGoods.getStockCount() - cartGoods.getNum());
        redisTemplate.boundHashOps("seckillGoods").put(seckillGoods.getGoodsId(),seckillGoods);

        // 3.保存订单数据
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置订单一分钟过期
        redisTemplate.opsForValue().set(orders.getId(),orders,1, TimeUnit.MINUTES);
        /**
         * 给订单创建副本,副本的过期时间长于原订单
         * redis过期后触发过期事件时,redis数据已经过期,此时只能拿到key,拿不到value。
         * 而过期事件需要回退商品库存,必须拿到value即订单详情,才能拿到商品数据,进行回退操作
         * 我们保存一个订单副本,过期时间长于原订单,此时就可以通过副本拿到原订单数据
         */
        redisTemplate.opsForValue().set(orders.getId()+"_copy",orders,2,TimeUnit.MINUTES);
        return orders;
    }

编写redis监听器,监听过期未支付订单 (RedisKeyExpirationListener.java和RedisListenerConfig.java)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

/**
 * redis监听器
 */
@Configuration
public class RedisListenerConfig {
    // 配置redis监听器,监听redis过期时间
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory){
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }
}

 

订单过期后,关闭交易,回退商品库存

/**
 * redis监听类继承KeyExpirationEventMessageListener
 */
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SeckillService seckillService;

    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    /**
     * 订单过期后,关闭交易,回退商品库存
     * @param message
     * @param pattern
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        // 获取订单id
        String orderId = message.toString();

        // 拿到复制订单信息
        Orders orders = (Orders) redisTemplate.opsForValue().get(orderId + "_copy");
        Long goodId = orders.getCartGoods().get(0).getGoodId();//产品id
        Integer num = orders.getCartGoods().get(0).getNum();//产品数据

        // 查询秒杀商品
        SeckillGoods seckillGoods = seckillService.findSeckillGoodsByRedis(goodId);

        // 回退库存
        seckillGoods.setStockCount(seckillGoods.getStockCount()+num);
        redisTemplate.boundHashOps("seckillGoods").put(goodId,seckillGoods);

        // 删除复制订单数据
        redisTemplate.delete(orderId+"_copy");
    }
}

支付秒杀订单

/**
     * 支付秒杀订单
     * @param id 订单id
     * @return
     */
    @GetMapping("/pay")
    public BaseResult pay(String id){
        // 支付秒杀订单
        // 1.查询订单,设置相应数据
        Orders orders = (Orders) redisTemplate.opsForValue().get(orderId);
        if (orders == null){
            throw new BusException(CodeEnum.ORDER_EXPIRED_ERROR);
        }
        orders.setStatus(2);
        orders.setPaymentTime(new Date());
        orders.setPaymentType(2); // 支付宝支付

        // 2.从redis删除订单
        redisTemplate.delete(orderId);
        redisTemplate.delete(orderId+"_copy");

        // 将订单存入数据库
        orderService.add(orders);
        return BaseResult.ok();
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值