当前位置: 首页 > news >正文

Java秒杀系统设计

Java秒杀系统设计

系统大概的整体架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W3bvuDBe-1665453447906)(%E7%A7%92%E6%9D%80%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1.assets/image-20220927093908185.png)]

秒杀一般都是在高并发的场景下出现,这时候就需要考虑一些原则性的问题:

  1. 高并发

  2. 超卖

  3. 恶意请求

  4. 链接暴露

  5. 数据库

1.高并发

高并发是指在某一瞬间,一个接口突然收到爆发式请求,不做很好的限流保护,可能会造成系统的崩溃。

1.1生成订单代码

@Override
public void generateOrderByMq(GenerateOrderDto generateOrderDto) {
    log.info("------生成订单开始进行流量削峰-----");
    MqMessage<GenerateOrderMqDto> generateOrderDtoMqMessage = new MqMessage<>();
    GenerateOrderMqDto generateOrderMqDto = new GenerateOrderMqDto();
    generateOrderMqDto.setUserId(SecurityUtils.getUserId());
    generateOrderMqDto.setDeliveryType(generateOrderDto.getDeliveryType());
    generateOrderMqDto.setIsShopCar(generateOrderDto.getIsShopCar());
    generateOrderMqDto.setOneOrderDto(generateOrderDto.getOneOrderDto());
    generateOrderDtoMqMessage.setData(generateOrderMqDto);
    /*这里用mq的顺序队列 防止并发下库存出现混乱问题*/
    Boolean isSend = generateOrderProducer.sendOrderly(generateOrderDtoMqMessage);

}

1.2顺序消费生成者代码

package com.hjt.rocketmq.producer;

import com.hjt.constant.RocketMqConstant;
import com.hjt.message.MqMessage;
import com.hjt.myOrder.dto.GenerateOrderMqDto;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Objects;

/**
 * @author :hjt
 * @date : 2022/9/29
 */
@Component
public class GenerateOrderProducer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public Boolean sendOrderly(MqMessage<GenerateOrderMqDto> mqMessage) {
        // 有序消费
        SendResult sendResult = rocketMQTemplate.syncSendOrderly(RocketMqConstant.RQ_PRODUCER_GENERATE_ORDER_TOPIC, mqMessage, RocketMqConstant.RQ_PRODUCER_GENERATE_ORDER_KEY);
        SendStatus sendStatus = sendResult.getSendStatus();
        if (Objects.equals(sendStatus, SendStatus.SEND_OK)) {
            return true;
        }
        return false;
    }

}

1.3顺序队列消费者代码

package com.hjt.rocketmq.consumer;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.hjt.constant.RocketMqConstant;
import com.hjt.myOrder.dto.GenerateOrderMqDto;
import com.hjt.myOrder.service.impl.OrderServiceImpl;
import com.hjt.util.OrderPreventMqConsumerUtil;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author :hjt
 * @date : 2022/9/29
 */
@Component
@RocketMQMessageListener(topic = RocketMqConstant.RQ_PRODUCER_GENERATE_ORDER_TOPIC,
        consumerGroup = RocketMqConstant.RQ_CONSUMER_GENERATE_ORDER_CONSUMER_GROUP, consumeMode = ConsumeMode.ORDERLY
)
public class GenerateOrderConsumer implements RocketMQListener<Message> {

    private static final Logger log = LoggerFactory.getLogger(GenerateOrderConsumer.class);

    @Autowired
    private OrderPreventMqConsumerUtil orderPreventMqConsumerUtil;


    @Autowired
    private OrderServiceImpl orderServiceImpl;


    @Override
    public void onMessage(Message message) {
        //防止消息被重复消费
        MessageExt messageExt = (MessageExt) message;
        String msgId = messageExt.getMsgId();
        log.info("msgId:" + msgId);
        //判断是否消费过
        boolean consumerMQ = orderPreventMqConsumerUtil.isConsumerMQ(msgId);
        if(consumerMQ){
            log.error("topic: " + RocketMqConstant.RQ_TOPIC_DELAYED_ORDER + " msgId: " + msgId + " 已被消费");
            return;
        }
        log.info("--------开始进行生成订单处理-----");
        String strBody = orderPreventMqConsumerUtil.exchangeStr(message.getBody());
        log.info("strBody: {}", strBody);
        //json转换
        GenerateOrderMqDto mqDto = JSONUtil.toBean(strBody, GenerateOrderMqDto.class);
        if(ObjectUtil.isNull(mqDto)){
            log.error("-----生成订单实体类为空-----");
            return;
        }
        /*这里是生成订单的业务逻辑*/
        Boolean isSuccess = orderServiceImpl.generateOrder(mqDto);
        if(!isSuccess){
            log.error("====生成订单信息失败===");
        }
        /*标记消息未已消费*/
        orderPreventMqConsumerUtil.addConsumerMQ(msgId);
    }

}

1.4生成订单代码

@Override
public Boolean generateOrder(GenerateOrderMqDto generateOrderMqDto) {
    //初始化返回类型
    List<ProductDto> products = new ArrayList<>();
    Long countAllPro = 0L;
    //计算总商品金额
    BigDecimal totalMoney = BigDecimal.ZERO;
    //商品id,多的以,分割
    String countAllProIds = "";

    List<OneOrderDto> oneOrderDto = generateOrderMqDto.getOneOrderDto();
    if (CollectionUtils.isEmpty(oneOrderDto)) {
        log.error("订单信息不能为空");
        return false;
    }
    //先根据雪花算法生成订单id
    Long orderId = snowFlake.nextId();
    //遍历计算每个商品对应的金额或者库存等等。。。
    for (int i = 0; i < oneOrderDto.size(); i++) {
        OneOrderDto oneOrder = oneOrderDto.get(i);
        //初始化商品
        Product product = null;
        //先查下该商品是否有库存
        LambdaQueryWrapper<Product> lambdaProduct = new LambdaQueryWrapper<>();
        lambdaProduct.eq(Product::getIsDelete, 0);
        lambdaProduct.eq(Product::getId, oneOrder.getProId());
        product = productMapper.selectOne(lambdaProduct);
        //商品已下架
        if (ObjectUtil.isNull(product)) {
            log.error("订单模块,商品id:" + oneOrder.getProId() + "已下架");
            return false;
        }
        //判断商品是否有库存
        if (product.getStock() <= 0) {
            log.error("订单模块,商品id:" + oneOrder.getProId() + "没有库存,请重新选择");
            return false;
        }

        /**对该商品进行加锁*/
        if (!versionLockUtil.versionLockReduceStock(product, lambdaProduct, oneOrder.getCount())) {
            log.error("订单模块,商品id:" + oneOrder.getProId() + "版本号机制获取库存失败,请稍微再试");
            return false;
        }

        //计算商品总金额
        countAllPro = countAllPro + oneOrder.getCount();

        //金额计算
        BigDecimal proCount = new BigDecimal(oneOrder.getCount());
        BigDecimal totalOnePro = product.getPrice().multiply(proCount);
        totalMoney = totalMoney.add(totalOnePro);
        //统计商品id
        countAllProIds = countAllProIds + String.valueOf(oneOrder.getProId()) + ",";

        //初始化
        ProductDto productDto = new ProductDto();
        productDto.setId(oneOrder.getProId());
        productDto.setStock(oneOrder.getCount());
        productDto.setPrice(product.getPrice());
        productDto.setProTitle(product.getProTitle());
        productDto.setOrderId(orderId);
        productDto.setUserId(SecurityUtils.getUserId());
        products.add(productDto);

        /*插入订单商品信息表*/
        OrderProductInfo orderProductInfo = new OrderProductInfo();
        orderProductInfo.setUserId(SecurityUtils.getUserId());
        orderProductInfo.setOrderId(orderId);
        orderProductInfo.setCount(oneOrder.getCount());
        orderProductInfo.setProductId(oneOrder.getProId());
        orderProductInfoMapper.insert(orderProductInfo);
    }
    Order oneOrder = new Order();
    oneOrder.setOrderId(orderId);
    //TODO 防止重复具体后续再实现

    //用rocketmq处理延迟付款
    MqMessage<List<ProductDto>> mqMessage = new MqMessage<>();
    mqMessage.setData(products);
    //  超时 没付款 回退库存 
    delayedOrderProducer.producerDelayedOrderSendOne(orderId, mqMessage);

   //初始化订单信息
    oneOrder.setCreateTime(LocalDateTime.now());
    oneOrder.setUserId(generateOrderMqDto.getUserId());
    oneOrder.setDeliveryType(generateOrderMqDto.getDeliveryType());
    //金额计算
    oneOrder.setTotal(totalMoney);
    oneOrder.setAllCount(countAllPro);
    oneOrder.setProId(countAllProIds);
    //未支付
    oneOrder.setIsPayed(false);
    oneOrder.setDeleteStatus(0);
    oneOrder.setVersion(0);
    orderMapper.insert(oneOrder);
    log.info("--------------订单信息初始化成功");

    /**异步修改商品库存*/
    this.asyncUpdateStockProducer(generateOrderMqDto.getOneOrderDto());
    return true;

}

需要注意的问题:

  1. mq队列不可以自定义抛出异常,例如:throw new BaseException(AuthException.AUTH_ERROR_MOBILE_EXIST);如果这样,mq会一直重试mq(重试16次,该消息会被重试16次,业务逻辑处理不好的话,会造成16次重复消费,导致业务上出现逻辑问题。)博主认为正确的做法应该是 记录错误的信息,然后就return返回了,后续再单独对这些错误的信息进行业务处理。

  2. mq防止被重复消费,这里博主用的是新建一张表,用一张表的主键去判断是否被消费过。(其实也可以用redis中set数据类型防止重复消费)

    ps:博主用上述代码,用JMeter测压测过1s1000000个请求生成订单也没问题。库存正常减少,包括生成订单后没付款也自己加回库存。

2.超卖解决方案

加乐观锁(为了效率问题抛弃悲观锁,且悲观锁可能会造成死锁问题)

乐观锁代码

/***
 * 版本号机制 更新库存(减库存)
 * @param product
 * @param lambdaProduct
 * @param count 商品库存
 */
public Boolean versionLockReduceStock(Product product, LambdaQueryWrapper<Product> lambdaProduct, Long count) {
    /*乐观锁  版本号机制*/
    /*用redis存版本号*/
    Long version = getVersion(product.getId());
    Product newProduct = productMapper.selectOne(lambdaProduct);
    Long newVersion = getVersion(newProduct.getId());

    /*redis获取库存*/
    Long stock = 0L;
    stock = getStock(product.getId());
    /*版本号一致就更新*/
    if (version.equals(newVersion)) {
        log.info("-----版本号一致,商品id为:" + product.getId() + "--- 版本号为:" + version);
        if (stock < count) {
            log.error("====当前商品没有足够的库存=====");
            return false;
        }
        stock = stock - count;
        log.info("当前库存为:" + newProduct);
        product.setStock(stock);
        redisUtil.set(RedisConstants.MODULES_ORDER_GENERATE_VERSION_KEY + String.valueOf(newProduct.getId()), newVersion + 1);
        /*redis修改库存*/
        if (redisUtil.set(RedisConstants.MODULES_ORDER_GENERATE_STOCK_KEY + newProduct.getId(), stock)) {
            return true;
        } else {
            return false;
        }
    }
    /*版本号不一致*/
    else {
        log.error("-----版本号不一致,商品id为:" + product.getId() + "--- 版本号为:" + version);
        /**睡眠2s 有3次机会*/
        for (int i = 0; i < 3; i++) {
            log.error("第:" + 1 + "次机会重试");
            ThreadUtil.safeSleep(2 * 1000);
            Product newestProduct = productMapper.selectOne(lambdaProduct);
            this.versionLockReduceStock(newestProduct, lambdaProduct, count);
        }

    }
    return false;
}

乐观锁建议是Version版本可以用redis来控制,用mysql中表的字段的话还需要update,如果请求多的时候,更新的语句其实本质上也是加锁,会造成程序的性能不高。

当然也可以用redisson的分布式锁,看门狗机制还是挺香的。

3.恶意请求(用Sentinel做限流)

sentinel详解介绍可以看我另外一篇文章。

用的是Sentinel进行请求保护

Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

建议nacos要和Sentinel结合,防止出现配置丢失

(ps:Sentinel如何安装使用请看我另外一篇博客)

/***
 * 秒杀流量削峰,避免1s收到很多请求
 * 这里用Sentinel防止生成订单被冲击
 */
@ApiOperation(value = "秒杀流量削峰,避免1s收到很多请求,生成订单(包含多个订单)")
@RequestMapping(method = RequestMethod.POST,value = "/generateOrderByMq")
@Log(title = "秒杀流量削峰,避免1s收到很多请求,生成订单(包含多个订单)")
@SentinelResource(value = "generateOrderByMq", blockHandler = "handleException",blockHandlerClass = OrderSentinelHandler.class)
public AjaxResult generateOrderByMq(@Valid @RequestBody GenerateOrderDto generateOrderDto) {
    orderService.generateOrderByMq(generateOrderDto);
    return AjaxResult.success();
}

主要代码

@SentinelResource(value = "generateOrderByMq", blockHandler = "handleException",blockHandlerClass = OrderSentinelHandler.class)
/**
 * @author :hjt
 * @date : 2022/10/10
 * Sentinel 自定义异常处理类
 * 异常状态码 8000-9000
 */
public class OrderSentinelHandler  {

    public static AjaxResult handleException(BlockException exception){
        return new AjaxResult(8001,"生成订单请求次数过多,请稍后再试");
    }
}

这里设置
在这里插入图片描述
QPS=并发量/平均响应时间

200只是个粗略的数值,具体可以根据你的业务进行调整。

4.链接暴露问题

链接加密(博主采用的是RSA加密算法)
具体加密算法可以看我这篇博客Java加密算法

这是请求订单的接口(加密后的参数这是前端直接传给我们的,这里我们模拟的是前端加密的场景)
Java代码例子

   //加密
    @Encrypt
    @PostMapping("/encryption")
    public GenerateOrderDto encryption(@Valid @RequestBody GenerateOrderDto generateOrderDto){
        GenerateOrderDto generateOrderDto1 = new GenerateOrderDto();
//        BeanUtil.copyProperties(generateOrderDto,generateOrderDto1);
        long time = System.currentTimeMillis();
        generateOrderDto.setTimestamp(time);
        return generateOrderDto;
    }
	//解密
    @Decrypt
    @PostMapping("/decryption")
    public AjaxResult Decryption(@RequestBody GenerateOrderDto generateOrderDto){
        return AjaxResult.success(generateOrderDto.toString());
    }

友情提示:
详情代码可以看下面我的个人项目地址 。https://github.com/hongjiatao/spring-boot-anyDemo

在这里插入图片描述
后端拿到参数进行解密
在这里插入图片描述
需要注意的是,再次请求该解密的接口,会报错。
在这里插入图片描述

这里博主还设置了一个参数只能被解密一次,也就是说前端传过来的加密参数只能用一次,这样做的好处就是即使黑客人员拿到我们加密后的参数,也无法大批量的去请求订单接口,有效的防止订单接口被黑客利用工具直接请求。

TODO // 2022/10/14写

5.数据库问题怎么防止被击穿。

后续补上。

个人搭建项目代码地址:
https://github.com/hongjiatao/spring-boot-anyDemo

欢迎收藏点赞三连。谢谢!有问题可以留言博主会24小时内无偿回复。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【C语言】自定义类型(构造类型)——结构体、枚举和联合体
  • 《SpringBoot篇》19.SpringBoot整合Quart
  • 一把搞懂线程中stop、sleep、supend、yield、wait、notify
  • 初阶数据结构 栈
  • Redis实战 - 08 Redis 的 BitMaps 位图命令
  • 改进YOLOv5系列:首发结合最新CSPNeXt主干结构(适用YOLOv7),高性能,低延时的单阶段目标检测器主干,通过COCO数据集验证高效涨点
  • 基于导频的信道估计实现
  • 小满Vue3第四十五章(Vue3 Web Components)
  • Linux 内核页表管理
  • 基于MAX-SUM算法的大规模信息系统的协调问题matlab仿真
  • 【Spring+SpringMVC+Mybatis】Spring+SpringMVC+Mybatis实现前端到后台完整项目
  • c#,c++,qt中多线程访问UI控件线程的问题汇总
  • 详解预处理指令(#define)
  • JS第五课(JS的分支语句)
  • 【项目实战】自主实现HTTP(七)——错误处理、线程池引入、项目扩展及结项
  • 【140天】尚学堂高淇Java300集视频精华笔记(86-87)
  • CAP 一致性协议及应用解析
  • CentOS学习笔记 - 12. Nginx搭建Centos7.5远程repo
  • crontab执行失败的多种原因
  • ECMAScript 6 学习之路 ( 四 ) String 字符串扩展
  • ES学习笔记(10)--ES6中的函数和数组补漏
  • flutter的key在widget list的作用以及必要性
  • Github访问慢解决办法
  • gops —— Go 程序诊断分析工具
  • Hexo+码云+git快速搭建免费的静态Blog
  • HTTP--网络协议分层,http历史(二)
  • IDEA常用插件整理
  • JavaScript异步流程控制的前世今生
  • js面向对象
  • MySQL用户中的%到底包不包括localhost?
  • nginx 配置多 域名 + 多 https
  • Redis字符串类型内部编码剖析
  • Spring思维导图,让Spring不再难懂(mvc篇)
  • 从零到一:用Phaser.js写意地开发小游戏(Chapter 3 - 加载游戏资源)
  • 机器人定位导航技术 激光SLAM与视觉SLAM谁更胜一筹?
  • 码农张的Bug人生 - 见面之礼
  • 那些被忽略的 JavaScript 数组方法细节
  • 使用 Xcode 的 Target 区分开发和生产环境
  • 硬币翻转问题,区间操作
  • 走向全栈之MongoDB的使用
  • ​Spring Boot 分片上传文件
  • (01)ORB-SLAM2源码无死角解析-(66) BA优化(g2o)→闭环线程:Optimizer::GlobalBundleAdjustemnt→全局优化
  • (20)目标检测算法之YOLOv5计算预选框、详解anchor计算
  • (4)事件处理——(6)给.ready()回调函数传递一个参数(Passing an argument to the .ready() callback)...
  • (51单片机)第五章-A/D和D/A工作原理-A/D
  • (C语言)共用体union的用法举例
  • (delphi11最新学习资料) Object Pascal 学习笔记---第14章泛型第2节(泛型类的类构造函数)
  • (Matalb时序预测)WOA-BP鲸鱼算法优化BP神经网络的多维时序回归预测
  • (Matlab)使用竞争神经网络实现数据聚类
  • (二)fiber的基本认识
  • (附源码)springboot课程在线考试系统 毕业设计 655127
  • (十六)Flask之蓝图
  • (学习日记)2024.04.10:UCOSIII第三十八节:事件实验
  • *++p:p先自+,然后*p,最终为3 ++*p:先*p,即arr[0]=1,然后再++,最终为2 *p++:值为arr[0],即1,该语句执行完毕后,p指向arr[1]
  • .equal()和==的区别 怎样判断字符串为空问题: Illegal invoke-super to void nio.file.AccessDeniedException