8.用户抢购


业务核心关键

使用redis的锁操作对库存、用户等属性进行加锁解锁的操作实现抢购逻辑

业务流程

抢购业务流程图-89


抢购业务实现

  • 加了一万个校验的购买流程
  • springboot、dobbu、activemq、redis
  • 用户点击购买->初始化库存->锁用户->库存数量校验->锁库存->生成内部订单->排队生成外部订单->等待订单生成->扣库存->释放库存锁

web模块负责:

  1. 从用户点击购买->生成内部订单
  2. 初始化库存:需要后端库存入Redis(jpa、redis)
  3. 锁用户(redis)
  4. 库存数量校验(redis)
  5. 锁库存(redis)
  6. 生成内部订单
  7. 等待订单生成(redis)
  8. 扣库存(redis)
  9. 释放库存锁(redis)

provider模块负责:

  1. 排队生成外部订单(activemq)

以下是详细代码:

web模块:

依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!-- dubbo begin springboot 1.5.9,对应dubbo 0.1.0 -->
<dependency>
    <groupId>com.alibaba.boot</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>${dubbo.version}</version>
</dependency>
<!-- dubbo end -->

<!-- zkclient begin -->
<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>${zkclient.version}</version>
</dependency>
<!-- zkclient end -->

<!-- api模块是consumer和provider公共依赖-->
<dependency>
    <groupId>com.lcywings.sbt</groupId>
    <artifactId>buy-limit-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- api end -->
<!-- processor begin -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<!-- processor end -->
配置:
# 应用名称
spring.application.name=buy-limit-web
# 端口号
server.port=8090
# dubbo+zookeeper配置
dubbo.application.name=edoc-consumer
# 注册中心地址
dubbo.registry.address=127.0.0.1:2181
dubbo.registry.protocol=zookeeper
dubbo.scan.base-packages=com.lcywings.sbt

# redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379

# 抢购业务自定义系统配置
# 商品库存初始数量,实际开发不会配置
buy.limit.prodStockNum=100
# 商品库存存放redis的标识前缀
buy.limit.stockNumPrefix=prod-stock-num-
#锁定用户的标识前缀
buy.limit.lockUerPrefix=lock-user-
#初始化抢购活动的时长
buy.limit.buyLimitTime=1800
#商品库存锁定标识前缀
buy.limit.lockProdStockPrefix=lock-prod-stock-
# 商品库存锁定时长
buy.limit.lockProdStockTime=60
# 生成订单超时时长
buy.limit.tradeOrderLimitTime=30

BuyLimitConfig

package com.lcywings.sbt.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * Created on 2021/8/12.
 * <p>
 * Author : Lcywings
 * <p>
 * Description : 抢购自动配置类
 */
@Component
@ConfigurationProperties(prefix = "buy.limit")
@Data
public class BuyLimitConfig {

    /**
     * 初始库存数量
     */
    private Integer prodStockNum;
    /**
     * 库存数量的前缀
     */
    private String stockNumPrefix;
    /**
     * 锁定用户的标识前缀
     */
    private String lockUerPrefix;
    /**
     * 抢购活动的时长
     */
    private Integer buyLimitTime;

    /**
     * 商品库存锁定标识前缀
     */
    private String lockProdStockPrefix;
    /**
     * 商品库存锁定时长
     */
    private Integer lockProdStockTime;

    /**
     * 生成订单超时时长
     */
    private Integer tradeOrderLimitTime;

}


BuyLimitController

package com.lcywings.sbt.controller;

import com.alibaba.dubbo.config.annotation.Reference;
import com.lcywings.sbt.api.TradeOrderApi;
import com.lcywings.sbt.service.BuyLimitService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

/**
 * Created on 2021/8/12.
 * <p>
 * Author : Lcywings
 * <p>
 * Description : 抢购业务入口
 */
@RestController
@Slf4j
public class BuyLimitController {


    @Autowired
    private BuyLimitService buyLimitService;

    @Reference
    private TradeOrderApi tradeOrderApi;

    /**
     * @author : Lcywings
     * @date : 2021/8/12 11:21
     * @acl : true
     * @description : 开始抢购入口
     */
    @RequestMapping(value = "/buyLimit", method = {RequestMethod.GET, RequestMethod.POST})
    public String buyLimit(@RequestParam String userId, @RequestParam String prodId, @RequestParam Integer buyCount) throws Exception {

        log.info("------ 0 开始抢购,用户编号{},商品编号{},购买数量{} ------", userId, prodId, buyCount);

        // 模拟初始化商品库存数量到redis中,实际开发过程中,是后台初始化到Redis
        log.warn("------ 1 初始化商品{}的库存数量 ------", prodId);
        if (!buyLimitService.initProdStorkToRedis(prodId)) {
            log.warn("------ 1 初始化商品:{},库存数量失败或者是已经存在 ------", prodId);
        }

        // 锁定用户,抢购业务针对同一个用户,在当前商品整个抢购过程中,只能抢购一次
        log.warn("------ 2 校验当前抢购用户:{},是否重复购买/是否成功抢购过改商品 ------", userId, prodId);
        if (!buyLimitService.lockBuyLimitUser(userId, prodId)) {
            log.warn("------ 2 同一个用户:{},抢购商品{},只能抢购一次 ------", userId, prodId);
            return "抢购失败,同一个商品的抢购活动中,同一个用户只能抢购一次!";
        }

        // 校验库存,个人库存不足,直接返回抢购失败
        log.warn("------ 3 校验当前抢购用户:{},抢购商品:{},库存数量是否充足 -------", prodId, buyCount);
        if (!buyLimitService.checkProdStock(prodId, buyCount)) {
            log.warn("------ 3 商品:{},购买数量:{},库存不足 -------", prodId, buyCount);
            return "抢购失败,当前商品库存数量不足!";
        }

        // 锁库存,如果当前锁库存操作失败,说明库存已经被其他用户锁住了,必须等待释放库存锁
        while (true) {
            // 循环判断库存锁是不是已经被释放,只有释放了库存锁,才可以进行后续流程操作(为了防止库存锁释放一次,必须给库存锁加过期时长)
            if (!buyLimitService.isProdStockLocked(prodId)) {
                log.info("------ 4 锁定商品库存成功,用户编号:{},商品编号:{} ------", userId, prodId);
                break;
            }
        }

        // 模拟生成订单,省略实体参数
        String tradeOrderNo = "T" + DateTimeFormatter.ofPattern("yyMMddHHmmss").format(LocalDateTime.now()) + UUID.randomUUID().toString().replace("-", "").substring(0, 6);

        log.info("----- 5 开始生成订单,用户:{},商品:{},购买数量:{},订单号:{} ------", userId, prodId, buyCount, tradeOrderNo);

        // 将当前订单信息存入redis
        buyLimitService.setTradeOrderTORedis(userId, prodId, tradeOrderNo);

        // 调用订单中心接口,生成订单,在订单接口信息
        tradeOrderApi.createBuyLimitTradeOrder(userId, prodId, tradeOrderNo);

        // 等待订单生成成功
        while (true) {
            // 判断订单是否生成成功
            int createStatus = buyLimitService.getCreatedTradeOrderStatus(userId, prodId);
            if (createStatus == 1) {
                log.info("----- 6 订单生成成功,用户:{},商品:{},购买数量:{},订单号:{} ------", userId, prodId, buyCount, tradeOrderNo);
                break;
            } else if (createStatus == 2 || createStatus == 3) {
                // 订单生成失败或者异常
                log.warn("----- 7 订单生成失败,用户:{},商品:{},购买数量:{},订单号:{} ------", userId, prodId, buyCount, tradeOrderNo);
                // 释放锁住用户
                buyLimitService.unlockBuyLimitUser(userId, prodId);

                // 释放库存锁(不用扣减库存)
                buyLimitService.unlockProdStock(prodId);
                return "抢购失败,请重新操作!";
            }
        }

        // 订单生成成功,扣减库存
        buyLimitService.subProdStockNum(prodId, buyCount);

        // 释放库存锁
        buyLimitService.unlockProdStock(prodId);

        log.info("------ 抢购成功,用户编号:{},商品编号:{},购买数量:{} ------", userId, prodId, buyCount);

        return "抢购成功!订单号为:" + tradeOrderNo;
    }

}


BuyLimitService

package com.lcywings.sbt.service;

/**
 * Created on 2021/8/12.
 * <p>
 * Author : Lcywings
 * <p>
 * Description : 抢购业务接口
 */
public interface BuyLimitService {

    /**
     * @author : Lcywings
     * @date : 2021/8/12 11:24
     * @acl : true
     * @description : 初始化商品库存数量到redis
     */
    boolean initProdStorkToRedis(String prodId);

    /**
     * @author : Lcywings
     * @date : 2021/8/12 11:47
     * @acl : true
     * @description : 锁定商品的抢购用户
     */
    boolean lockBuyLimitUser(String userId, String prodId);

    /**
     * @author : Lcywings
     * @date : 2021/8/12 14:05
     * @acl : true
     * @description : 校验商品库存是否充足
     */
    boolean checkProdStock(String prodId, Integer buyCount);

    /**
     * @author : Lcywings
     * @date : 2021/8/12 14:13
     * @acl : true
     * @description : 校验库存追否已被锁定,没有就上锁,并增加超时时长
     */
    boolean isProdStockLocked(String prodId);

    /**
     * @author : Lcywings
     * @date : 2021/8/12 14:29
     * @acl : true
     * @description : 将订单信息存入redis
     */
    void setTradeOrderTORedis(String userId, String prodId, String tradeOrderNo);

    /**
     * @author : Lcywings
     * @date : 2021/8/13 9:14
     * @acl : true
     * @description : 家宴订单生成状态,0正在生成,1生成成功,3超时取消
     */
    int getCreatedTradeOrderStatus(String userId, String prodId);

    /**
     * @author : Lcywings
     * @date : 2021/8/13 9:41
     * @acl : true
     * @description : 释放锁住的用户
     */
    void unlockBuyLimitUser(String userId, String prodId);

    /**
     * @author : Lcywings
     * @date : 2021/8/13 9:44
     * @acl : true
     * @description : 释放库存锁
     */
    void unlockProdStock(String prodId);

    /**
     * @author : Lcywings
     * @date : 2021/8/13 9:48
     * @acl : true
     * @description : 抢购成功,扣减库存
     */
    void subProdStockNum(String prodId, Integer buyCount);
}

BuyLimitServiceImpl

package com.lcywings.sbt.service.impl;

import com.lcywings.sbt.config.BuyLimitConfig;
import com.lcywings.sbt.service.BuyLimitService;
import com.lcywings.sbt.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * Created on 2021/8/12.
 * <p>
 * Author : Lcywings
 * <p>
 * Description : 抢购业务实现类
 */
@Service
@Slf4j
public class BuyLimitServiceImpl implements BuyLimitService {


    @Autowired
    private BuyLimitConfig buyLimitConfig;

    @Autowired
    private RedisUtils redisUtils;

    @Override
    public boolean initProdStorkToRedis(String prodId) {

        // 如果redis中已经初始化过商品库存数量,不需要重复操作,如果没有才需要初始化
        return redisUtils.get(buyLimitConfig.getStockNumPrefix() + prodId) == null
                && redisUtils.set(buyLimitConfig.getStockNumPrefix() + prodId, buyLimitConfig.getProdStockNum());
    }

    @Override
    public boolean lockBuyLimitUser(String userId, String prodId) {
        // 使用redis分布式锁,锁定当前商品的抢购用户,借助名称空间
        return redisUtils.lock(buyLimitConfig.getLockUerPrefix() + prodId + ":" + userId, null, buyLimitConfig.getBuyLimitTime());

    }

    @Override
    public boolean checkProdStock(String prodId, Integer buyCount) {
        // 从redis获取商品库存数量
        Object prodStockNum = redisUtils.get(buyLimitConfig.getStockNumPrefix() + prodId);
        return prodStockNum != null && Integer.valueOf(prodStockNum.toString()) >= buyCount;
    }

    @Override
    public boolean isProdStockLocked(String prodId) {
        // 直接使用分布式锁,上锁,如果上锁失败,说明已经被锁定,如果上锁成功,没有被锁,可以进行抢购
        return !redisUtils.lock(buyLimitConfig.getLockProdStockPrefix() + prodId, null, buyLimitConfig.getLockProdStockTime());
    }

    @Override
    public void setTradeOrderTORedis(String userId, String prodId, String tradeOrderNo) {
        // 使用用户编号作为唯一标识(方便前端根用户编号判断订单是否生成成功)
        Map<String, Object> tradeOrderMap = new HashMap<>();
        tradeOrderMap.put("tradeOrderNo ", tradeOrderNo);
        // 订单状态为0,代表正在生成订单
        tradeOrderMap.put("tradeOrderStatus", 0);
        redisUtils.hmset(userId + "-" + prodId, tradeOrderMap, buyLimitConfig.getTradeOrderLimitTime());
    }

    @Override
    public int getCreatedTradeOrderStatus(String userId, String prodId) {
        // 判断订单是否超时(生成订单存入redis有个限制时间,超出时间自动删除),没有订单号,覆盖订单状态为超时取
        if (!redisUtils.hasKey(userId + "-" + prodId)) {
            redisUtils.hset(userId + "-" + prodId, "tradeOrderStatus", 3);
        }

        // 如果没有超时,获取订单状态(先判断redis中是否真的没有超时,防止上一步还没超时,到这一步超时)
        if (!ObjectUtils.isEmpty(redisUtils.hget(userId + "-" + prodId, "tradeOrderStatus"))) {
            return (int) redisUtils.hget(userId + "-" + prodId, "tradeOrderStatus");
        }

        // 返回订单正在生成标识,继续等待
        return 0;
    }

    @Override
    public void unlockBuyLimitUser(String userId, String prodId) {
        // 删除锁用户
        redisUtils.delLock(buyLimitConfig.getLockProdStockPrefix() + userId + ":" + prodId);
    }

    @Override
    public void unlockProdStock(String prodId) {
        // 删除锁库存
        redisUtils.delLock(buyLimitConfig.getLockProdStockPrefix() + prodId);
    }

    @Override
    public void subProdStockNum(String prodId, Integer buyCount) {
        //抢购成功,减少库存数量
        redisUtils.decr(buyLimitConfig.getStockNumPrefix() + prodId, buyCount);
    }
}




provider模块:

依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!-- dubbo begin springboot 1.5.9,对应dubbo 0.1.0 -->
<dependency>
    <groupId>com.alibaba.boot</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>${dubbo.version}</version>
</dependency>
<!-- dubbo end -->

<!-- zkclient begin -->
<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>${zkclient.version}</version>
</dependency>
<!-- zkclient end -->

<!-- api模块是consumer和provider公共依赖-->
<dependency>
    <groupId>com.lcywings.sbt</groupId>
    <artifactId>buy-limit-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- api end -->

<!-- activemq begin -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<!-- activemq end -->
配置:
# 应用名称
spring.application.name=buy-limit-provider
# 端口号
server.port=8089
# dubbo+zookeeper配置
dubbo.application.name=edoc-provider
# 注册中心地址
dubbo.registry.address=127.0.0.1:2181
dubbo.registry.protocol=zookeeper
dubbo.protocol.port=-1


# activemq配置
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=admin
spring.activemq.password=admin
# 信任所有包,保证消息消息对象在传输过程中,能争取序列化成功
spring.activemq.packages.trust-all=true
# 默认情况下,ActiveMQ使用的模式是队列Queue,如果要是用主题队列模式Topic模式,必须改默认配置
# 如果单独开主题模式,会导致系统只能使用主题,不可以使用点对点模式
#spring.jms.pub-sub-domain=true

# redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379

TradeOrderProvider

package com.lcywings.sbt.provider;

import com.alibaba.dubbo.config.annotation.Service;
import com.lcywings.sbt.api.TradeOrderApi;
import lombok.extern.slf4j.Slf4j;
import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsMessagingTemplate;

import javax.jms.Destination;
import java.util.HashMap;
import java.util.Map;

/**
 * Created on 2021/8/13.
 * <p>
 * Author : Lcywings
 * <p>
 * Description : 抢购西单服务提供者
 */
@Service //阿里的
@Slf4j
public class TradeOrderProvider implements TradeOrderApi {
    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;


    @Override
    public void createBuyLimitTradeOrder(String userId, String prodId, String tradeOrderNo) throws Exception {
        log.info("****** 收到生成订单请求,用户编号:{},商品编号:{},交易订单号:{}, ******", userId, prodId, tradeOrderNo);
        // TODO 收到下单请求,必须要做参数校验,用户信息获取,商品信息获取,创建订单实体对象等待

        log.info("****** 将生成订单请求发送到异步生成订单消息队列中 ******");

        // 创建消息队列
        Destination destination = new ActiveMQQueue("buy-limit-create-order-queue");

        // 创建消息对象
        Map<String, String> paramMap = new HashMap<>();
        paramMap.put("userId", userId);
        paramMap.put("prodId", prodId);
        paramMap.put("tradeOrderNo", tradeOrderNo);

        // 发送消息队列,注意:使用高可用的消息队列-主从或者集群,防止队列不可用或者消息丢失
        jmsMessagingTemplate.convertAndSend(destination, paramMap);

    }
}

TradeOrderListener

package com.lcywings.sbt.listener;

import com.lcywings.sbt.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * Created on 2021/8/13.
 * <p>
 * Author : Lcywings
 * <p>
 * Description : 监听生成订单的消息队列,生成订单
 */
@Component
@Slf4j
public class TradeOrderListener {

    @Autowired
    private RedisUtils redisUtils;

    /**
     * @author : Lcywings
     * @date : 2021/8/13 9:03
     * @acl : true
     * @description : 从生成抢购订单队列中,获取生成订单请求,开始生成订单
     */
    @JmsListener(destination = "buy-limit-create-order-queue")
    public void createTradeOrderFromQueue(Map<String, String> paramsMap) {

        log.info("###### 开始生成订单,请求详情:{}", paramsMap);

        // TODO 开始生成订单,存入数据库,存入缓存redis中等

        // 生成订单成功,修改redis中抢购的订单的状态为1,生成失败或者异常,改为2
        redisUtils.hset(paramsMap.get("userId") + "-" + paramsMap.get("prodId"), "tradeOrderStatus", 1);

    }
}

Q.E.D.