V2-V3的变化【秒杀项目】

  • 前言
  • 版权
  • 推荐
  • V2~V3的变化
  • 总结流程
  • 配置
  • pom.xml
  • application-dev.properties
  • comment
  • ErrorCode
  • configuration
  • WebMvcConfiguration
  • emtity
  • ItemStockLog
  • mapper
  • ItemStockLogMapper
  • ItemStockLogMapper.xml
  • controller
  • OrderController
  • service
  • ItemService
  • PromotionService
  • OrderService
  • rocket
  • DecreaseStockConsumer
  • LocalTransactionListenerImpl
  • 最后


前言

2023-8-17 13:03:47


推荐

V2~V3的变化

总结流程

OrderController
	//获取验证码
	order/captcha

    //验证码正确后
    //获取秒杀凭证
    //底层调用promotionService.generateToken()
    order/token

    //下单操作
    //限制单击流量
    //获取活动凭证
    //加入队列等待
    //调用底层的orderService.createOrderAsync()
    order/create
    
OrderService
createOrderAsync()
    //异步创建订单
    //执行本地事务


rocket
    //执行本地事务
    executeLocalTransaction()
    //创建订单
    //调用orderService.createOrder()    
	createOrder()
	
    //检查本地事务
    checkLocalTransaction
    //检查流水
    //调用itemService.findItemStorkLogById()
    checkStockStatus()

    //执行Mysql中的扣减库存
    //调用itemService.decreaseStock()
	onMessage()
PromotionService
    //判断售罄标识
    //获取秒杀令牌
    //v.decrement("promotion:gate:" + promotionId, 1) < 0
    //设置秒杀凭证
    //v.set(key, token, 10, TimeUnit.MINUTES);
	generateToken()

OrderService
    //订单的创建
    //异步扣减库存
    //调用itemService.decreaseStockInCache()
    //生成订单
    //更新销量
	createOrder()

itemService
/*
    //预扣库存
    //redisTemplate.opsForValue().decrement(key, amount);
    //售罄标识
    //redisTemplate.opsForValue().set("item:stock:over:" + itemId, 1);
	decreaseStockInCache()
*/

    //使用Lua脚本
    //得到stock
    //判断stock>amount
    //DECRBY stockKey, amount
    //SET stockOverKey, 1
	decreaseStockInCache()
	
	//itemStockLogMapper.selectByPrimaryKey(id);
	findItemStorkLogById()
	
	//itemStockMapper.decreaseStock(itemId, amount);
	decreaseStock()

配置

pom.xml

<dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>com.github.whvcse</groupId>
            <artifactId>easy-captcha</artifactId>
            <version>1.6.2</version>
        </dependency>

application-dev.properties

# rocketmq
rocketmq.name-server=192.168.253.160:9876
rocketmq.producer.group=seckill_producer


# ThreadPool
spring.task.execution.pool.core-size=5
spring.task.execution.pool.queue-capacity=10
spring.task.execution.pool.max-size=30

comment

ErrorCode

int CREATE_ORDER_FAILURE = 202;
	int OUT_OF_LIMIT = 203;

configuration

WebMvcConfiguration

//  registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/order/create");
	registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/order/captcha","/order/token", "/order/create");

emtity

ItemStockLog

mapper

ItemStockLogMapper

ItemStockLogMapper.xml

controller

OrderController

package com.nowcoder.seckill.controller;

@Controller
@RequestMapping("/order")
@CrossOrigin(origins = "${nowcoder.web.path}", allowedHeaders = "*", allowCredentials = "true")
public class OrderController implements ErrorCode {

    private Logger logger = LoggerFactory.getLogger(OrderController.class);

    private RateLimiter rateLimiter = RateLimiter.create(1000);

    @Autowired
    private OrderService orderService;

    @Autowired
    private PromotionService promotionService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;

    //获取验证码
    @RequestMapping(path = "/captcha", method = RequestMethod.GET)
    public void getCaptcha(String token, HttpServletResponse response) {
        SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4);

        if (token != null) {
            User user = (User) redisTemplate.opsForValue().get(token);
            if (user != null) {
                String key = "captcha:" + user.getId();
                redisTemplate.opsForValue().set(key, specCaptcha.text(), 1, TimeUnit.MINUTES);
            }
        }

        response.setContentType("image/png");
        try {
            OutputStream os = response.getOutputStream();
            specCaptcha.out(os);
        } catch (IOException e) {
            logger.error("发送验证码失败:" + e.getMessage());
        }
    }
    
    //获取秒杀凭证
    //底层调用promotionService.generateToken()
    @RequestMapping(path = "/token", method = RequestMethod.POST)
    @ResponseBody
    public ResponseModel generateToken(int itemId, int promotionId, String token, String captcha) {
        User user = (User) redisTemplate.opsForValue().get(token);

        if (StringUtils.isEmpty(captcha)) {
            throw new BusinessException(PARAMETER_ERROR, "请输入正确的验证码!");
        }

        String key = "captcha:" + user.getId();
        String realCaptcha = (String) redisTemplate.opsForValue().get(key);
        if (!captcha.equalsIgnoreCase(realCaptcha)) {
            throw new BusinessException(PARAMETER_ERROR, "请输入正确的验证码!");
        }

        String promotionToken = promotionService.generateToken(user.getId(), itemId, promotionId);//底层存入promotionToken到redis中
        if (StringUtils.isEmpty(promotionToken)) {
            throw new BusinessException(CREATE_ORDER_FAILURE, "下单失败!");
        }
        return new ResponseModel(promotionToken);
    }


//    @RequestMapping(path = "/create", method = RequestMethod.POST)
//    @ResponseBody
//    public ResponseModel create(/*HttpSession session, */
//            int itemId, int amount, Integer promotionId, String token) {
        User user = (User) session.getAttribute("loginUser");
//        User user = (User) redisTemplate.opsForValue().get(token);
//        orderService.createOrder(user.getId(), itemId, amount, promotionId);
//        return new ResponseModel();
//    }

    //下单操作
    //调用底层的orderService.createOrderAsync()
    @RequestMapping(path = "/create", method = RequestMethod.POST)
    @ResponseBody
    public ResponseModel create(/*HttpSession session, */
            int itemId, int amount, Integer promotionId, String promotionToken, String token) {

        //限制单机流量
        if (!rateLimiter.tryAcquire(1,TimeUnit.SECONDS)) {
            throw new BusinessException(OUT_OF_LIMIT, "服务器繁忙,请稍后再试!");
        }

//        User user = (User) session.getAttribute("loginUser");
        User user = (User) redisTemplate.opsForValue().get(token);
        logger.debug("登录用户 [" + token + ": " + user + "]");

        //验证活动凭证
        if (promotionId != null) {
            String key = "promotion:token:" + user.getId() + ":" + itemId + ":" + promotionId;
            String realPromotionToken = (String) redisTemplate.opsForValue().get(key);
            if (StringUtils.isEmpty(promotionToken) || !promotionToken.equals(realPromotionToken)) {
                throw new BusinessException(CREATE_ORDER_FAILURE, "下单失败!");
            }
        }

        //加入队列等待
        Future future = taskExecutor.submit(new Callable() {
            @Override
            public Object call() throws Exception {
//              orderService.createOrder(user.getId(), itemId, amount, promotionId);
                orderService.createOrderAsync(user.getId(), itemId, amount, promotionId);
                return null;
            }
        });

        //验证处理结果
        try {
            future.get();
        } catch (Exception e) {
            throw new BusinessException(UNDEFINED_ERROR, "下单失败!");
        }

        return new ResponseModel();
    }
}

service

ItemService

ItemStockLog createItemStockLog(int itemId, int amount);

    void updateItemStockLogStatus(String id, int status);

    ItemStockLog findItemStorkLogById(String id);

PromotionService

package com.nowcoder.seckill.service.impl;

@Service
public class PromotionServiceImpl implements PromotionService, ErrorCode {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private UserService userService;

    @Autowired
    private ItemService itemService;

    @Override
    public String generateToken(int userId, int itemId, int promotionId) {
        if (userId < 0 || itemId < 0 || promotionId < 0) {
            return null;
        }

        // 售罄标识
        if (redisTemplate.hasKey("item:stock:over:" + itemId)) {
            return null;
        }

        // 校验用户
        User user = userService.findUserFromCache(userId);
        if (user == null) {
            return null;
        }

        // 校验商品
        Item item = itemService.findItemInCache(itemId);
        if (item == null) {
            return null;
        }

        // 校验活动
        if (item.getPromotion() == null
                || !item.getPromotion().getId().equals(promotionId)
                || item.getPromotion().getStatus() != 0) {
            return null;
        }

        // 秒杀大闸
        ValueOperations v = redisTemplate.opsForValue();
        if (v.decrement("promotion:gate:" + promotionId, 1) < 0) {
            return null;
        }

        String key = "promotion:token:" + userId + ":" + itemId + ":" + promotionId;
        String token = UUID.randomUUID().toString().replace("-", "");
        v.set(key, token, 10, TimeUnit.MINUTES);

        return token;
    }

}

OrderService

package com.nowcoder.seckill.service.impl;


@Service
public class OrderServiceImpl implements OrderService, ErrorCode {

    private Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private SerialNumberMapper serialNumberMapper;

    @Autowired
    private UserService userService;

    @Autowired
    private ItemService itemService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 格式:日期 + 流水
     * 示例:20210123000000000001
     *
     * @return
     */
    //订单号的创建
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public String generateOrderID() {
        StringBuilder sb = new StringBuilder();

        // 拼入日期
        sb.append(Toolbox.format(new Date(), "yyyyMMdd"));

        // 获取流水号
        SerialNumber serial = serialNumberMapper.selectByPrimaryKey("order_serial");
        Integer value = serial.getValue();

        // 更新流水号
        serial.setValue(value + serial.getStep());
        serialNumberMapper.updateByPrimaryKey(serial);

        // 拼入流水号
        String prefix = "000000000000".substring(value.toString().length());
        sb.append(prefix).append(value);

        return sb.toString();
    }
    
    //订单的创建
    //调用itemService.decreaseStockInCache()
    @Transactional
    public Order createOrder(int userId, int itemId, int amount, Integer promotionId, String itemStockLogId) {
        // 校验参数
        if (amount < 1 || (promotionId != null && promotionId.intValue() <= 0)) {
            throw new BusinessException(PARAMETER_ERROR, "指定的参数不合法!");
        }

        // 校验用户
//        User user = userService.findUserById(userId);
        User user = userService.findUserFromCache(userId);
        if (user == null) {
            throw new BusinessException(PARAMETER_ERROR, "指定的用户不存在!");
        }

        // 校验商品
//        Item item = itemService.findItemById(itemId);
        Item item = itemService.findItemInCache(itemId);
        if (item == null) {
            throw new BusinessException(PARAMETER_ERROR, "指定的商品不存在!");
        }

        // 校验库存
        int stock = item.getItemStock().getStock();
        if (amount > stock) {
            throw new BusinessException(STOCK_NOT_ENOUGH, "库存不足!");
        }

        // 校验活动
        if (promotionId != null) {
            if (item.getPromotion() == null) {
                throw new BusinessException(PARAMETER_ERROR, "指定的商品无活动!");
            } else if (!item.getPromotion().getId().equals(promotionId)) {
                throw new BusinessException(PARAMETER_ERROR, "指定的活动不存在!");
            } else if (item.getPromotion().getStatus() != 0) {
                throw new BusinessException(PARAMETER_ERROR, "指定的活动未开始!");
            }
        }

        // 扣减库存
        // 增加行锁:前提是商品主键字段上必须有索引。
        // 缓存库存:异步同步数据库并保证最终一致性。
//        boolean successful = itemService.decreaseStock(itemId, amount);
        boolean successful = itemService.decreaseStockInCache(itemId, amount);//在redis缓存中扣减
        logger.debug("预扣减库存完成 [" + successful + "]");
        if (!successful) {
            throw new BusinessException(STOCK_NOT_ENOUGH, "库存不足!");
        }

        // 生成订单
        Order order = new Order();
        order.setId(this.generateOrderID());
        order.setUserId(userId);
        order.setItemId(itemId);
        order.setPromotionId(promotionId);
        order.setOrderPrice(promotionId != null ? item.getPromotion().getPromotionPrice() : item.getPrice());
        order.setOrderAmount(amount);
        order.setOrderTotal(order.getOrderPrice().multiply(new BigDecimal(amount)));
        order.setOrderTime(new Timestamp(System.currentTimeMillis()));
        orderMapper.insert(order);
        logger.debug("生成订单完成 [" + order.getId() + "]");

        // 更新销量
//        itemService.increaseSales(itemId, amount);
//        logger.debug("更新销量完成 [" + itemId + "]");
        JSONObject body = new JSONObject();
        body.put("itemId", itemId);
        body.put("amount", amount);
        Message msg = MessageBuilder.withPayload(body.toString()).build();
        rocketMQTemplate.asyncSend("seckill:increase_sales", msg, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                logger.debug("投递增加商品销量消息成功");
            }

            @Override
            public void onException(Throwable e) {
                logger.error("投递增加商品销量消息失败", e);
            }
        }, 60 * 1000);

        // 更新库存流水状态
        itemService.updateItemStockLogStatus(itemStockLogId, 1);
        logger.debug("更新流水完成 [" + itemStockLogId + "]");

        return order;
    }

    //异步创建订单
    //执行本地事务 
    @Override
    public void createOrderAsync(int userId, int itemId, int amount, Integer promotionId) {
        // 售罄标识
        if (redisTemplate.hasKey("item:stock:over:" + itemId)) {
            throw new BusinessException(STOCK_NOT_ENOUGH, "已经售罄!");
        }

        // 生成库存流水
        ItemStockLog itemStockLog = itemService.createItemStockLog(itemId, amount);//初始状态是0
        logger.debug("生成库存流水完成 [" + itemStockLog.getId() + "]");

        // 消息体
        JSONObject body = new JSONObject();
        body.put("itemId", itemId);
        body.put("amount", amount);
        body.put("itemStockLogId", itemStockLog.getId());

        // 本地事务参数
        JSONObject arg = new JSONObject();
        arg.put("userId", userId);
        arg.put("itemId", itemId);
        arg.put("amount", amount);
        arg.put("promotionId", promotionId);
        arg.put("itemStockLogId", itemStockLog.getId());

        String dest = "seckill:decrease_stock";
        Message msg = MessageBuilder.withPayload(body.toString()).build();
        try {
            logger.debug("尝试投递扣减库存消息 [" + body.toString() + "]");
            TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(dest, msg, arg);
            if (sendResult.getLocalTransactionState() == LocalTransactionState.UNKNOW) {
                throw new BusinessException(UNDEFINED_ERROR, "创建订单失败!");
            } else if (sendResult.getLocalTransactionState() == LocalTransactionState.ROLLBACK_MESSAGE) {
                throw new BusinessException(CREATE_ORDER_FAILURE, "创建订单失败!");
            }
        } catch (MessagingException e) {
            throw new BusinessException(CREATE_ORDER_FAILURE, "创建订单失败!");
        }
    }
}

rocket

DecreaseStockConsumer

package com.nowcoder.seckill.rocket.consumer;

@Service
@RocketMQMessageListener(topic = "seckill",
        consumerGroup = "seckill_stock", selectorExpression = "decrease_stock")
public class DecreaseStockConsumer implements RocketMQListener<String> {

    private Logger logger = LoggerFactory.getLogger(DecreaseStockConsumer.class);

    @Autowired
    private ItemService itemService;

    @Override
    public void onMessage(String message) {
        JSONObject param = JSONObject.parseObject(message);
        int itemId = (int) param.get("itemId");
        int amount = (int) param.get("amount");

        try {
            itemService.decreaseStock(itemId, amount);
            logger.debug("最终扣减库存完成 [" + param.get("itemStockLogId") + "]");
        } catch (Exception e) {
            logger.error("从DB扣减库存失败", e);
        }
    }

}

LocalTransactionListenerImpl

package com.nowcoder.seckill.rocket.producer;


@Service
@RocketMQTransactionListener
public class LocalTransactionListenerImpl implements RocketMQLocalTransactionListener {

    private Logger logger = LoggerFactory.getLogger(LocalTransactionListenerImpl.class);

    @Autowired
    private OrderService orderService;

    @Autowired
    private ItemService itemService;

    //执行本地事务
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            String tag = msg.getHeaders().get("rocketmq_TAGS").toString();
            if ("decrease_stock".equals(tag)) {
                return this.createOrder(msg, arg);//调用方法
            } else {
                return RocketMQLocalTransactionState.UNKNOWN;
            }
        } catch (Exception e) {
            logger.error("执行MQ本地事务时发生错误", e);
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    //检查本地事务
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        try {
            String tag = (String) msg.getHeaders().get("rocketmq_TAGS");
            if ("decrease_stock".equals(tag)) {
                return this.checkStockStatus(msg);//调用方法
            } else {
                return RocketMQLocalTransactionState.UNKNOWN;
            }
        } catch (Exception e) {
            logger.error("检查MQ本地事务时发生错误", e);
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    //创建订单
    private RocketMQLocalTransactionState createOrder(Message msg, Object arg) {
        JSONObject param = (JSONObject) arg;

        int userId = (int) param.get("userId");
        int itemId = (int) param.get("itemId");
        int amount = (int) param.get("amount");
        int promotionId = (int) param.get("promotionId");
        String itemStockLogId = (String) param.get("itemStockLogId");

        try {
            Order order = orderService.createOrder(userId, itemId, amount, promotionId, itemStockLogId);//调用service创建
            logger.debug("本地事务提交完成 [" + order.getId() + "]");
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            logger.error("创建订单失败", e);
            itemService.updateItemStockLogStatus(itemStockLogId, 3);
            logger.debug("更新流水完成 [" + itemStockLogId + "]");
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    //检查流水
    private RocketMQLocalTransactionState checkStockStatus(Message msg) {
        JSONObject body = JSONObject.parseObject(new String((byte[]) msg.getPayload()));
        String itemStockLogId = (String) body.get("itemStockLogId");
        ItemStockLog itemStockLog = itemService.findItemStorkLogById(itemStockLogId);
        logger.debug("检查事务状态完成 [" + itemStockLog + "]");
        if (itemStockLog == null) {
            return RocketMQLocalTransactionState.ROLLBACK;
        } else if (itemStockLog.getStatus() == 0) {
            return RocketMQLocalTransactionState.UNKNOWN;
        } else if (itemStockLog.getStatus() == 1) {
            return RocketMQLocalTransactionState.COMMIT;
        } else {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

}

最后

我们都有光明的未来

祝大家考研上岸
祝大家工作顺利
祝大家得偿所愿
祝大家如愿以偿
点赞收藏关注哦