热门商品抢单实现

156 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情

每日英语:

Laughter is an instant vacation.

笑是即刻的假期。 -米尔顿·伯利

热门商品抢单

热门商品抢单分为2个步骤:

1:需要识别商品是否为热门商品,如果是热门商品则需要排队,排队需要向RocketMQ发送MQ消息。
2:下单需要从排队信息中获取抢单信息,并执行下单操作。

接下来我们按这2个步骤实现。

1 抢单排队

排队的时间节点很重要,不要在后端微服务排队,后端服务排队会降低整个服务的性能,我们可以选择在代理层排队(Nginx)或者Api网关层排队(Gateway),由于大家的专长是Java,我们选择在API网关排队。

1611539718656.png

1)引入依赖

mall-api-gateway的pom.xml中引入如下依赖:

<dependencies>
    <!--Redis-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
​
    <!--rocketmq-->
    <dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-spring-boot-starter</artifactId>
        <version>2.0.2</version>
    </dependency>
</dependencies>

2)MQ、Redis配置

修改mall-api-gateway的bootstrap.yml文件,添加RocketMQ和Redis配置:

spring:
  redis:
    host: 192.168.xxx.xxx
    port: 6379
#producer
rocketmq:
  name-server: 192.168.xxx.xxx:9876
  producer:
    group: hotorder-group
    send-message-timeout: 300000
    compress-message-body-threshold: 4096
    max-message-size: 4194304
    retry-times-when-send-async-failed: 0
    retry-next-server: true
    retry-times-when-send-failed: 2

3)热门商品排队

mall-api-gateway中创建com.xz.mall.api.hot.HotQueue在该类中进行排队操作:

@Component
public class HotQueue {
​
    @Autowired
    private RedisTemplate redisTemplate;
​
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
​
    //商品非热门
    public static final Integer NOT_HOT=0;
    //已经在排队中
    public static final Integer HAS_QUEUE=204;
    //排队成功
    public static final Integer QUEUE_ING=200;
​
​
    /***
     * 抢单排队
     * username:用户名
     * id:商品ID
     * num:件数
     */
    public int hotToQueue(String username,String id,Integer num){
        //获取该商品在Redis中的信息,如果Redis中存在对应信息,热门商品
        Boolean bo = redisTemplate.boundHashOps("HotSeckillGoods").hasKey(id);
        if(!bo){
            //商品非热门
            return NOT_HOT;
        }
        //避免重复排队
        Long increment = redisTemplate.boundValueOps("OrderQueue" + username).increment(1);
        if(increment>1){
            //请勿重新排队
            return HAS_QUEUE;
        }
​
        //执行排队操作
        Map<String,Object> dataMap = new HashMap<String,Object>();
        dataMap.put("username",username);
        dataMap.put("id",id);
        dataMap.put("num",num);
        Message<String> message = MessageBuilder.withPayload(JSON.toJSONString(dataMap)).build();
        rocketMQTemplate.convertAndSend("order-queue",message);
        return QUEUE_ING;
    }
}

ApiFilter中调用上面的排队方法:

@Autowired
private HotQueue hotQueue;
​
/***
 * 执行拦截处理      http://localhost:9001/mall/seckill/order?id&num
 *                 JWT
 * @param exchange
 * @param chain
 * @return
 */
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpRequest request = exchange.getRequest();
    //用户名
    String username = "xz";
    //商品ID
    String id = request.getQueryParams().getFirst("id");
    //数量
    Integer num =Integer.valueOf( request.getQueryParams().getFirst("num") );
​
    //排队结果
    int result = hotQueue.hotToQueue(username, id, num);
​
    //QUEUE_ING、HAS_QUEUE
    if(result==HotQueue.QUEUE_ING || result==HotQueue.HAS_QUEUE){
        //响应状态码200
        Map<String,Object> resultMap = new HashMap<String,Object>();
        resultMap.put("type","hot");
        resultMap.put("code",result);
        exchange.getResponse().setStatusCode(HttpStatus.OK);
        exchange.getResponse().setComplete();
        exchange.getResponse().getHeaders().add("message",JSON.toJSONString(resultMap));
    }
​
    //NOT_HOT 直接由后端服务处理
    return chain.filter(exchange);
}

2 排队监听

用户排队后,我们需要对用户排队的信息进行下单操作,此时需要监听排队信息,我们在mall-seckill-service中编写监听操作,用于监听消息。

1)引入依赖

修改mall-seckill-service中的pom.xml添加rocketmq依赖:

<!--rocketmq-->
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.0.2</version>
</dependency>

2)配置MQ信息

修改bootstrap.yml添加rocketmq配置:

#mq
rocketmq:
  name-server: 192.168.xxx.xxx:9876

3)监听创建

创建com.xz.mall.seckill.mq.OrderQueueListener实现对排队抢单信息监听:

@RocketMQMessageListener(
        topic = "order-queue",                  //topic:和消费者发送的topic相同
        consumerGroup = "orderqueue-consumer",     //group:不用和生产者group相同
        selectorExpression = "*")               //tag
@Component
public class OrderQueueListener implements RocketMQListener {
​
    /***
     * 排队信息
     * @param message
     */
    @Override
    public void onMessage(Object message) {
        System.out.println("排队信息:"+message);
    }
}

3 热门商品抢单实现

抢单实现我们需要监听队列信息,并实现下单操作,同时如果抢单后商品库存为0,则需要同步到数据库中。

1)Service

接口:修改com.xz.mall.seckill.service.SeckillOrderService添加热门商品下单方法:

/***
 * 热门商品抢单操作
 */
int add(Map<String,Object> dataMap);

实现类:修改com.xz.mall.seckill.service.impl.SeckillOrderServiceImpl实现热门商品下单:

@Service
public class SeckillOrderServiceImpl extends ServiceImpl<SeckillOrderMapper,SeckillOrder> implements SeckillOrderService {
​
    //库存不足
    public static final int STORE_NOT_FULL=0;
    //库存足够下单成功
    public static final int STORE_FULL_ORDER_SUCCESS=1;
​
​
    @Autowired
    private SeckillOrderMapper seckillOrderMapper;
​
    @Autowired
    private RedisTemplate redisTemplate;
​
    @Autowired
    private SeckillGoodsMapper seckillGoodsMapper;
​
​
    /***
     * 热门商品抢单实现
     * @return
     */
    @Override
    public int add(Map<String,Object> dataMap) {
        //username
        String username = dataMap.get("username").toString();
        //id
        String id = dataMap.get("id").toString();
        //num
        Integer num =Integer.valueOf(dataMap.get("num").toString() );
​
        /**
         * 库存足够
         */
        Object storecount = redisTemplate.boundHashOps("HotSeckillGoods").get(id);
        if(storecount==null || Integer.valueOf(storecount.toString())<num){
            //移除排队标识
            redisTemplate.delete("OrderQueue"+username);
            return STORE_NOT_FULL;
        }
​
        //查询商品信息
        SeckillGoods seckillGoods = seckillGoodsMapper.selectById(id);
​
        /***
         * 添加订单
         */
        SeckillOrder seckillOrder = new SeckillOrder();
        seckillOrder.setUsername(username);
        seckillOrder.setSeckillGoodsId(id);
        seckillOrder.setCreateTime(new Date());
        seckillOrder.setMoney(seckillGoods.getSeckillPrice()*num);
        seckillOrder.setNum(num);
        seckillOrder.setStatus(0);  //下单了
        seckillOrderMapper.insert(seckillOrder);
​
        /*****
         * 库存递减
         */
        Long lastStoreCount = redisTemplate.boundHashOps("HotSeckillGoods").increment(id, -num);
​
        if(lastStoreCount==0){
            //将数据同步到数据库
            seckillGoods = new SeckillGoods();
            seckillGoods.setId(id);
            seckillGoods.setStoreCount(0);
            seckillGoodsMapper.updateById(seckillGoods);
            //删除Redis缓存
            redisTemplate.boundHashOps("HotSeckillGoods").delete(id);
        }
        //移除排队标识
        redisTemplate.delete("OrderQueue"+username);
​
        return STORE_FULL_ORDER_SUCCESS;
    }
}

4 抢单超卖控制

在热门商品抢单过程中,存在超卖现象,即便是MQ监听下单,但MQ消费组有可能会有集群,各个组集群消费时,判断商品库存有可能同时出现判断商品库存为1的时候,这时候就容易存在超卖现象。

我们可以采用基于Redis的分布式锁来实现。

1)引入依赖包

<!--Redisson分布式锁-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.8.2</version>
</dependency>

2)创建RedissonClient

创建com.xz.mall.seckill.lock.RedissonConfig用于配置当前使用的Redis服务

@Configuration
public class RedissonConfig {
​
    /***
     * 创建RedissonClient客户端
     * @return
     */
    public RedissonClient redisson(){
        Config config = new Config();
        //单机模式
        config.useSingleServer().setAddress("redis://192.168.xxx.xxx:6379");
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

3)超卖控制

修改com.xz.mall.seckill.service.impl.SeckillOrderServiceImpl的热门商品抢单方法,增加分布式锁控制:

@Autowired
private RedissonClient redissonClient;
​
​
/***
 * 热门商品抢单实现
 * @return
 */
@Override
public int add(Map<String,Object> dataMap) {
    //username
    String username = dataMap.get("username").toString();
    //id
    String id = dataMap.get("id").toString();
    //num
    Integer num =Integer.valueOf(dataMap.get("num").toString() );
​
    //获取锁
    RLock lock = redissonClient.getLock("No00001");
    lock.lock();
​
    try {
        /**
         * 库存足够  略...
         */
​
        //略...
​
        if(lastStoreCount==0){
            //略...
        }
        //移除排队标识
        redisTemplate.delete("OrderQueue"+username);
        //释放锁
        lock.unlock();
    } catch (NumberFormatException e) {
        //释放锁
        lock.unlock();
    }
    return STORE_FULL_ORDER_SUCCESS;
}

完整代码如下:

@Autowired
private RedissonClient redissonClient;
​
​
/***
 * 热门商品抢单实现
 * @return
 */
@Override
public int add(Map<String,Object> dataMap) {
    //username
    String username = dataMap.get("username").toString();
    //id
    String id = dataMap.get("id").toString();
    //num
    Integer num =Integer.valueOf(dataMap.get("num").toString() );
​
    //获取锁
    RLock lock = redissonClient.getLock("No00001");
    lock.lock();
​
    try {
        /**
         * 库存足够
         */
        Object storecount = redisTemplate.boundHashOps("HotSeckillGoods").get(id);
        if(storecount==null || Integer.valueOf(storecount.toString())<num){
            //移除排队标识
            redisTemplate.delete("OrderQueue"+username);
            return STORE_NOT_FULL;
        }
​
        //查询商品信息
        SeckillGoods seckillGoods = seckillGoodsMapper.selectById(id);
​
        /***
         * 添加订单
         */
        SeckillOrder seckillOrder = new SeckillOrder();
        seckillOrder.setUsername(username);
        seckillOrder.setSeckillGoodsId(id);
        seckillOrder.setCreateTime(new Date());
        seckillOrder.setMoney(seckillGoods.getSeckillPrice()*num);
        seckillOrder.setNum(num);
        seckillOrder.setStatus(0);  //下单了
        seckillOrderMapper.insert(seckillOrder);
​
        /*****
         * 库存递减
         */
        Long lastStoreCount = redisTemplate.boundHashOps("HotSeckillGoods").increment(id, -num);
​
        if(lastStoreCount==0){
            //将数据同步到数据库
            seckillGoods = new SeckillGoods();
            seckillGoods.setId(id);
            seckillGoods.setStoreCount(0);
            seckillGoodsMapper.updateById(seckillGoods);
            //删除Redis缓存
            redisTemplate.boundHashOps("HotSeckillGoods").delete(id);
        }
        //移除排队标识
        redisTemplate.delete("OrderQueue"+username);
        lock.unlock();
    } catch (NumberFormatException e) {
        lock.unlock();
    }
    return STORE_FULL_ORDER_SUCCESS;
}

总结

本篇主要讲述了一下热门商品抢单实现时,需要关注的几个问题,抢单排队、排队监听、抢单超卖控制实现。