库存扣减设计和下单

news2024/12/25 9:08:47

这里写目录标题

  • 前言
  • 正文
  • 库存设计原则
  • 常见库存扣减方案
  • 秒杀订单域设计
    • 整体服务
    • 领域模型
    • 领域服务
    • 领域事件之下单
      • 下单整体流程
      • 同步下单
      • 库存预扣减
      • 库存扣减
  • 总结
    • 参考链接


前言

大家好,我是练习两年半的Java练习生,前面我们已经介绍了领域驱动和缓存设计,这一章我们将介绍秒杀设计过程中比较重要的一个部分:关于库存扣减。我们将会从一些库存设计的一些原则和方案入手,介绍有哪些方案,进行对比,然后再分析秒杀系统中具体的下单代码,下单代码中就涉及到库存的扣减。让我们一起来看看吧~


正文

库存设计原则

  • 渠道隔离:秒杀活动中使用的库存应当按渠道进行隔离,这样既能保证不对正常售卖渠道产生影响,有利于精细化运作库存管理。比如,根据用户群体或平台支持的力度不同,我们可能需要在不同渠道透出不同的库存数量,并且它们与正常售卖渠道分离,这种场景下就需要渠道隔离;
  • 防止超卖:对业务来说,超卖可能意味着资损;对技术来说,超卖意味着架构的失败。试想,原价999元商品的秒杀价为599,库存100件却卖出了10000件,那么我们就会面临严重的客诉或资损;
  • 防止重复扣减:与超卖相对的是没有卖出去,其同样不可小觑。比如,10000件的库存仅有10人成单,库存明明还在却显示已经售罄,活动未到达预期,前期准备和推广的资金投入都打了水票,而由系统设计缺陷造成的重复扣减 就会导致这种糟糕的情况发生;
  • 高性能:在前面的文章我们谈到了如何通过缓存提高秒杀架构中的”读“性能,殊不知”写“性能也是秒杀架构的重要指标之一。举例来说,10000比订单,每秒写入300单和每秒写入3000单在用户体验上有着显著的差异。

常见库存扣减方案

(一)基于数据库的库存扣减方案:

直接更新:在数据库中直接更新库存字段的数值来进行扣减操作。这是一种简单直接的方案,但在高并发情况下可能存在并发冲突的问题。
悲观锁:使用数据库的悲观锁机制,在进行库存扣减操作时,锁定对应的库存记录,防止其他线程同时进行修改。通过数据库的锁机制确保一次只有一个线程能够进行库存扣减操作。
乐观锁:使用数据库的乐观锁机制,每次进行库存扣减操作时,先读取当前库存的版本号,然后在更新库存时检查版本号是否一致。乐观锁方案通常使用版本号、时间戳等机制来实现,避免了悲观锁的性能开销。

(二)基于缓存的库存扣减方案:
缓存预减:在缓存中存储库存的剩余数量,每次进行扣减操作时,先从缓存中读取当前库存数量,然后在缓存中更新扣减后的库存值。这种方案可以减少对数据库的访问次数和并发压力。
分布式锁 + 缓存:结合分布式锁和缓存,使用分布式锁确保在同一时刻只有一个线程能够进行库存扣减操作,同时使用缓存提高读取性能。

(三)分库分表库存扣减方案:
对于大规模的系统,可以采用分库分表的方式来进行库存扣减操作,将库存数据分散存储在多个数据库或数据表中,以提高系统的并发性能和扩展性。
当然,相较于前两种方案,虽然分库分表的优势明显,但具有更高的复杂性和实现成本。在并发量不是很高的情况下,不推荐使用这种方式。这部分会在后续文章中介绍。

(四)常用库存扣减方式

  • 下单扣库存:优势在于简单,链路短,性能好,缺点在于容易被恶意下单。活动刚开始,可能即被恶意下单清空库存;
  • 支付扣库存:优势在于可以控制恶意下单,最后得到库存的都是有效订单。当然,其缺点也较为明显,无法控制下单人数,用户需要在支付时再次确认库存;
  • 下单预扣库存,超时取消:相较于前两种方式,这种方式较为折中且有效,对于正常下单的用户来说抢单即是得到,对于恶意下单的来说,占据的库存会超时自动释放。

我们在这里会介绍 数据库 + 缓存得综合方案。在库存扣减方式上,我们采取的是下单扣库存,因为这种方式比较快。此外,对于恶意下单的用户,我们也可以通过安全风控和策略来进行识别和屏蔽。

秒杀订单域设计

整体服务

领域模型

package com.actionworks.flashsale.domain.model.entity;

import com.alibaba.fastjson.JSON;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

@Data
public class FlashOrder implements Serializable {

    /**
     * 订单ID , 雪花算法生成
     */
    private Long id;
    /**
     * 商品ID
     */
    private Long itemId;
    /**
     * 秒杀品标题
     */
    private String itemTitle;
    /**
     * 秒杀价
     */
    private Long flashPrice;
    /**
     * 活动ID
     */
    private Long activityId;
    /**
     * 下单商品数量
     */
    private Integer quantity;
    /**
     * 总金额
     */
    private Long totalAmount;
    /**
     * 订单状态
     */
    private Integer status;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 订单创建时间
     */
    private Date createTime;

    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }

    public boolean validateParamsForCreate() {
        if (itemId == null
                || activityId == null
                || quantity == null || quantity <= 0
                || totalAmount == null || totalAmount < 0) {
            return false;
        }
        return true;
    }
}

领域服务

  • 下单:订单域的核心服务;
  • 根据用户获取订单:当前用户可以获取个人所创建过的订单;
  • 根据ID获取订单详情:当前用户可以通过订单ID查看订单详情;
  • 根据ID取消订单:当用户不再需要订单时,可以根据ID执行取消操作。当然,是否能取消成功,要看具体的规则。

领域事件之下单

下单整体流程

下单的整理流程如下:

代码如下:

public class DefaultFlashOrderAppService implements FlashOrderAppService {
    private static final Logger logger = LoggerFactory.getLogger(DefaultFlashOrderAppService.class);
    private static final String PLACE_ORDER_LOCK_KEY = "PLACE_ORDER_LOCK_KEY";

    @Resource
    private FlashOrderDomainService flashOrderDomainService;
    @Resource
    private StockDeductionDomainService stockDeductionDomainService;
    @Resource
    private ItemStockCacheService itemStockCacheService;
    @Resource
    private DistributedLockFactoryService lockFactoryService;
    @Resource
    private SecurityService securityService;
    @Resource
    private PlaceOrderService placeOrderService;

    @Override
    @Transactional
    public AppSimpleResult<PlaceOrderResult> placeOrder(Long userId, FlashPlaceOrderCommand placeOrderCommand) {
        logger.info("placeOrder|下单|{},{}", userId, JSON.toJSONString(placeOrderCommand));

        // 检查userId和placeOrderCommand的有效性
        if (userId == null || placeOrderCommand == null || !placeOrderCommand.validateParams()) {
            throw new BizException(INVALID_PARAMS);
        }

        // 根据userId生成锁的键值
        String placeOrderLockKey = getPlaceOrderLockKey(userId);

        // 从锁工厂服务获取分布式锁
        DistributedLock placeOrderLock = lockFactoryService.getDistributedLock(placeOrderLockKey);

        try {
            // 尝试在5秒内获取锁,如果获取失败则返回失败结果
            boolean isLockSuccess = placeOrderLock.tryLock(5, 5, TimeUnit.SECONDS);
            if (!isLockSuccess) {
                return AppSimpleResult.failed(FREQUENTLY_ERROR.getErrCode(), FREQUENTLY_ERROR.getErrDesc());
            }

            // 使用安全服务进行风险检查
            boolean isPassRiskInspect = securityService.inspectRisksByPolicy(userId);
            if (!isPassRiskInspect) {
                logger.info("placeOrder|综合风控检验未通过|{}", userId);
                return AppSimpleResult.failed(PLACE_ORDER_FAILED);
            }

            // 调用placeOrderService执行实际的下单操作
            PlaceOrderResult placeOrderResult = placeOrderService.doPlaceOrder(userId, placeOrderCommand);
            if (!placeOrderResult.isSuccess()) {
                return AppSimpleResult.failed(placeOrderResult.getCode(), placeOrderResult.getMessage());
            }

            logger.info("placeOrder|下单完成|{}", userId);
            return AppSimpleResult.ok(placeOrderResult);
        } catch (Exception e) {
            logger.error("placeOrder|下单失败|{},{}", userId, JSON.toJSONString(placeOrderCommand), e);
            return AppSimpleResult.failed(PLACE_ORDER_FAILED);
        } finally {
            placeOrderLock.unlock();
        }
    }

...
}

注意点:

  • 下单操作需要加对应的分布式锁。

分布式锁是为了解决分布式系统中的并发访问和数据一致性问题而引入的机制。在分布式系统中,多个节点同时访问共享资源时可能会导致数据不一致或竞态条件的问题。通过使用分布式锁,可以确保在同一时间只有一个节点能够访问和修改共享资源,从而保证数据的一致性和正确性。

同步下单

  • 下单有两种实现方式
    • 同步下单
    • 异步下单,

这里只介绍同步下单的方式,异步下单后续再介绍
image.png

同步下单的逻辑如下:

@Override
public PlaceOrderResult doPlaceOrder(Long userId, FlashPlaceOrderCommand placeOrderCommand) {
    logger.info("placeOrder|开始下单|{},{}", userId, JSON.toJSONString(placeOrderCommand));
    
    // 检查userId和placeOrderCommand的有效性
    if (userId == null || placeOrderCommand == null || !placeOrderCommand.validateParams()) {
        throw new BizException(INVALID_PARAMS);
    }
    
    // 检查秒杀活动是否允许下单
    boolean isActivityAllowPlaceOrder = flashActivityAppService.isAllowPlaceOrderOrNot(placeOrderCommand.getActivityId());
    if (!isActivityAllowPlaceOrder) {
        logger.info("placeOrder|秒杀活动下单规则校验未通过|{},{}", userId, placeOrderCommand.getActivityId());
        return PlaceOrderResult.failed(PLACE_ORDER_FAILED);
    }
    
    // 检查秒杀商品是否允许下单
    boolean isItemAllowPlaceOrder = flashItemAppService.isAllowPlaceOrderOrNot(placeOrderCommand.getItemId());
    if (!isItemAllowPlaceOrder) {
        logger.info("placeOrder|秒杀品下单规则校验未通过|{},{}", userId, placeOrderCommand.getActivityId());
        return PlaceOrderResult.failed(PLACE_ORDER_FAILED);
    }
    
    // 获取秒杀商品信息
    AppSimpleResult<FlashItemDTO> flashItemResult = flashItemAppService.getFlashItem(placeOrderCommand.getItemId());
    if (!flashItemResult.isSuccess() || flashItemResult.getData() == null) {
        return PlaceOrderResult.failed(ITEM_NOT_FOUND);
    }
    FlashItemDTO flashItem = flashItemResult.getData();
    
    // 生成订单号并创建待下单的FlashOrder对象
    Long orderId = orderNoGenerateService.generateOrderNo(new OrderNoGenerateContext());
    FlashOrder flashOrderToPlace = toDomain(placeOrderCommand);
    flashOrderToPlace.setItemTitle(flashItem.getItemTitle());
    flashOrderToPlace.setFlashPrice(flashItem.getFlashPrice());
    flashOrderToPlace.setUserId(userId);
    flashOrderToPlace.setId(orderId);
    
    // 创建库存扣减对象
    StockDeduction stockDeduction = new StockDeduction()
            .setItemId(placeOrderCommand.getItemId())
            .setQuantity(placeOrderCommand.getQuantity())
            .setUserId(userId);
    
    boolean preDecreaseStockSuccess = false;
    try {
        // 预扣减库存
        preDecreaseStockSuccess = itemStockCacheService.decreaseItemStock(stockDeduction);
        if (!preDecreaseStockSuccess) {
            logger.info("placeOrder|库存预扣减失败|{},{}", userId, JSON.toJSONString(placeOrderCommand));
            return PlaceOrderResult.failed(PLACE_ORDER_FAILED.getErrCode(), PLACE_ORDER_FAILED.getErrDesc());
        }
        
        // 真正扣减库存
        boolean decreaseStockSuccess = stockDeductionDomainService.decreaseItemStock(stockDeduction);
        if (!decreaseStockSuccess) {
            logger.info("placeOrder|库存扣减失败|{},{}", userId, JSON.toJSONString(placeOrderCommand));
            return PlaceOrderResult.failed(PLACE_ORDER_FAILED.getErrCode(), PLACE_ORDER_FAILED.getErrDesc());
        }
        
        // 下单操作
        boolean placeOrderSuccess = flashOrderDomainService.placeOrder(userId, flashOrderToPlace);
        if (!placeOrderSuccess) {
            throw new BizException(PLACE_ORDER_FAILED.getErrDesc());
        }
    } catch (Exception e) {
        // 下单过程中出现异常
        if (preDecreaseStockSuccess) {
            // 如果预扣减库存成功,则尝试恢复库存
            boolean recoverStockSuccess = itemStockCacheService.increaseItemStock(stockDeduction);
            if (!recoverStockSuccess) {
                logger.error("placeOrder|预扣库存恢复失败|{},{}", userId, JSON.toJSONString(placeOrderCommand), e);
            }
        }
        logger.error("placeOrder|下单失败|{},{}", userId, JSON.toJSONString(placeOrderCommand), e);
        throw new BizException(PLACE_ORDER_FAILED.getErrDesc());
    }
    
    logger.info("placeOrder|下单成功|{},{}", userId, orderId);
    return PlaceOrderResult.ok(orderId);
}

同步下单中里面需要关注两个点:

  • 预扣减库存
  • 真正的库存扣减

库存预扣减

为什么需要预扣减库存呢?
前面已经分析过扣减库存几种方案了,忘记了可以回头看看

那么预扣减缓存是如何实现的呢?
其实这里也有两种方式。

  • 单数据库扣减缓存
  • 分库分桶扣减缓存

这里也只介绍单数据库扣减缓存的方式
下面这段代码就是预扣减的一个逻辑,其中受用到Lua脚本进行一个库存扣减。使用Lua脚本有什么好处呢?
使用 Lua 脚本的主要原因是实现原子性操作和减少网络开销。下面是 Lua 脚本的一些优点和使用场景:
原子性操作:Lua 脚本在 Redis 服务器端执行,以原子方式执行多个 Redis 命令,保证这些命令在执行期间不会被其他客户端的请求中断。这对于需要保持数据的一致性和避免并发竞争条件非常重要。
减少网络开销:通过将多个操作打包成一个脚本,在一次网络往返中执行多个命令,减少了网络传输的开销。相比于客户端单独发送多个命令,使用 Lua 脚本可以显著降低网络延迟,提高系统的性能和响应速度。
原子性扣减操作:对于一些复杂的操作,如库存扣减、分布式锁的获取释放等,使用 Lua 脚本可以保证这些操作的原子性。通过在脚本中执行多个命令,将它们作为一个单独的操作单元进行处理,避免了并发下的竞态条件和数据不一致性。
服务器端执行:Lua 脚本在 Redis 服务器端执行,减少了客户端的负担和复杂性。客户端只需发送一次脚本执行的请求,而无需关心具体的操作细节和顺序。
代码复用和封装:使用 Lua 脚本可以将一组 Redis 命令封装为一个可复用的脚本,简化了客户端代码的编写和维护。脚本可以在不同的场景中多次使用,提高了代码的可维护性和可重用性。

@Override
    public boolean decreaseItemStock(StockDeduction stockDeduction) {
        logger.info("decreaseItemStock|申请库存预扣减|{}", JSON.toJSONString(stockDeduction));

        // 检查stockDeduction对象的有效性
        if (stockDeduction == null || !stockDeduction.validate()) {
            return false;
        }

        try {
            // 构建缓存键
            String key1ItemStocksCacheKey = getItemStocksCacheKey(stockDeduction.getItemId());
            String key2ItemStocksCacheAlignKey = getItemStocksCacheAlignKey(stockDeduction.getItemId());
            List<String> keys = Lists.newArrayList(key1ItemStocksCacheKey, key2ItemStocksCacheAlignKey);

            // 使用Lua脚本进行库存扣减
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(DECREASE_ITEM_STOCK_LUA, Long.class);
            Long result = null;
            long startTime = System.currentTimeMillis();

            // 进行循环,直到得到结果或超过时间限制
            while ((result == null || result == IN_STOCK_ALIGNING) && (System.currentTimeMillis() - startTime) < 1500) {
                // 调用Redis的execute方法执行Lua脚本,传入键和扣减数量作为参数
                result = redisCacheService.getRedisTemplate().execute(redisScript, keys, stockDeduction.getQuantity());

                if (result == null) {
                    logger.info("decreaseItemStock|库存扣减失败|{}", key1ItemStocksCacheKey);
                    return false;
                }

                if (result == IN_STOCK_ALIGNING) {
                    logger.info("decreaseItemStock|库存校准中|{}", key1ItemStocksCacheKey);
                    Thread.sleep(20);
                }

                if (result == -1 || result == -2 || result == -3) {
                    logger.info("decreaseItemStock|库存扣减失败|{}", key1ItemStocksCacheKey);
                    return false;
                }

                if (result == 1) {
                    logger.info("decreaseItemStock|库存扣减成功|{}", key1ItemStocksCacheKey);
                    return true;
                }
            }
        } catch (Exception e) {
            logger.error("decreaseItemStock|库存扣减失败", e);
            return false;
        }

        return false;
    }

对应的Lua脚本

if (redis.call('exists', KEYS[2]) == 1) then
  return -9;
end;
if (redis.call('exists', KEYS[1]) == 1) then
    local stock = tonumber(redis.call('get', KEYS[1]));
    local num = tonumber(ARGV[1]);
    if (stock < num) then
        return -3;
    end;
    if (stock >= num) then
        redis.call('incrby', KEYS[1], 0 - num);
        return 1;
    end;
    return -2;
end;
return -1;

好了,到这里预扣减库存完成了,那么我们就应该执行真正的库存扣减了
这里也是有分库分表的方式,但本文中只讲最基本的方式

库存扣减

代码:

@Override
    public boolean decreaseItemStock(StockDeduction stockDeduction) {
        if (stockDeduction == null || stockDeduction.getItemId() == null || stockDeduction.getQuantity() == null) {
        throw new DomainException(PARAMS_INVALID);
    }
return flashItemRepository.decreaseItemStock(stockDeduction.getItemId(), stockDeduction.getQuantity());
}

库存扣减的mapper:

<update id="decreaseItemStock" parameterType="com.actionworks.flashsale.persistence.model.FlashItemDO">
  UPDATE flash_item
  SET modified_time   = now(),
  available_stock = available_stock - #{quantity}
  where id = #{itemId}
  and available_stock <![CDATA[ >= ]]>  #{quantity}
</update>

这里要注意要保证不会扣减负库存,所以得保证有库存得前提下才进行库存扣减

但这段sql还是有问题的。
如果你对秒杀或高并发架构有所了解的话,可能会发现这句SQL并不完美。问题在于,它不是幂等的,在某些特殊情况下会发生重复扣减,这就违背本章节开篇的基本原则。那么,什么情况下会发生重复扣减?业务侧的代码不严谨地重试或底层的重试都会造成重复扣减。针对这个问题,有两个解决方案。
一是用设置库存取代扣库存,也就是将剩余库存在外部计算出来,并设置到数据库中,这样SQL就是幂等的:

<update id="decreaseItemStock" parameterType="com.actionworks.flashsale.persistence.model.FlashItemDO">
    UPDATE flash_item
    SET modified_time = now(),
    available_stock = #{newAvailableStock}
    stock_version = #{newStockVersion}
    where id = #{itemId} and stock_version = #{oldStockVersion}
</update>

二是通过CAS完成库存扣减,即在扣减库存的时候加上原始值。我们知道CAS是高效的无锁更新方式,在Java中有广泛应用,那么我们写个简单CAS:

<update id="decreaseItemStock" parameterType="com.actionworks.flashsale.persistence.model.FlashItemDO">
    UPDATE flash_item
    SET modified_time = now(),
    available_stock = available_stock - #{quantity}
    where id = #{itemId} and available_stock = #{oldAvailableStock}
</update>

虽然这两种方式能解决异常情况下的重复扣减问题,但服务端代码侧的复杂度也会相应地增加很多。

额外讲一下
关于事务@Transactional的作用和用法
作用:

  • 定义事务边界:@Transactional 注解将方法或类标记为一个事务,指定了方法或类内部的操作应该在一个单独的事务中执行。
  • 提供事务管理:@Transactional 注解使 Spring 能够自动管理事务的生命周期、事务的提交和回滚,以及异常处理。

当运行时候有未处理的异常抛出,那么会触发事务的回滚。
这部分在八股里面也应该会出现,需要大家关注


总结

好啦,以上就是本篇文章要介绍的所有内容,主要是将库存扣减的两种方式和下单的整体流程,认识秒杀设计的具体实现是怎么样的。如果大家还有什么问题
如果大家还有什么问题,欢迎私信或者在评论区提出来,让我们一起学习进步!或者加入我一起学习这个项目:https://github.com/jacky-curry/flash-sale
在这里插入图片描述


参考链接

https://juejin.cn/book/7008372989179723787/section/7020345200442605576

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/745840.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

路径规划算法:基于金豺优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于金豺优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于金豺优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化算法金豺…

【网络编程】应用层协议——HTTPS协议(数据的加密与解密)

文章目录 一、HTTP协议的缺陷二、HTTPS协议的介绍三、加密与解密3.1 加密与解密流程3.2 为什么要加密和解密3.3 常见的加密方式3.3.1 对称加密3.3.2 非对称加密3.3.3 数据摘要&#xff08;数据指纹&#xff09;3.3.4 数据签名 四、HTTPS工作过程4.1 中间人攻击方式4.2 数字证书…

50行PyTorch代码中的生成对抗网络(GAN)

一、说明 2014年,蒙特利尔大学的伊恩古德费罗(Ian Goodfellow)和他的同事发表了一篇令人惊叹的论文,向世界介绍了GANs或生成对抗网络。通过计算图和博弈论的创新组合,他们表明,如果有足够的建模能力,相互竞争的两个模型将能够通过普通的旧反向传播进行共同训练。 二、原…

大学啥也没有学到,跑到培训班里学技术,真的有用吗-以下来自一位认识的朋友投稿-王大师

在学习IT技术的过程中&#xff0c;你是否也被安利过各种五花八门的技术培训班&#xff1f;这些培训班都是怎样向你宣传的&#xff0c;你又对此抱有着怎样的态度呢&#xff1f;在培训班里学技术&#xff0c;真的有用吗&#xff1f;–王大师告诉你 1、掌握 JAVA入门到进阶知识(持…

极验滑块(3代)验证码细节避坑总结

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;不提供完整代码&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 关于 w 值…

惊!这道题正确率竟然只有 22%:答案详解

《Go 语言爱好者周刊》第 148 期有一道题目&#xff1a;以下代码输出什么&#xff1f; package mainimport ("fmt" )func main() {m : [...]int{a: 1,b: 2,c: 3,}m[a] 3fmt.Println(len(m)) }A&#xff1a;3&#xff1b;B&#xff1a;4&#xff1b;C&#xff1a;10…

面试官:过滤器和拦截器有什么区别?

过滤器&#xff08;Filter&#xff09;和拦截器&#xff08;Interceptor&#xff09;都是基于 AOP&#xff08;Aspect Oriented Programming&#xff0c;面向切面编程&#xff09;思想实现的&#xff0c;用来解决项目中某一类问题的两种“工具”&#xff0c;但二者有着明显的差…

AI原生云向量数据库Zilliz Cloud创建备份快照

目录 创建快照 调整快照保留天数 相关文档 快照是为某个集群在指定时间点创建的备份。您可以基于快照创建新的集群或将快照用作集群数据备份。 说明 快照功能目前仅对签约用户开放。如需使用该功能,请联系我们。 创建快照 快照创建是异步操作,创建所需时间取决于集群大…

Python爬虫学习笔记(一)————网页基础

目录 1.网页的组成 2.HTML &#xff08;1&#xff09;标签 &#xff08;2&#xff09;比较重要且常用的标签&#xff1a; ①列表标签 ②超链接标签 &#xff08;a标签&#xff09; ③img标签&#xff1a;用于渲染&#xff0c;图片资源的标签 ④div标签和span标签 &…

LabVIEW开发BROOKS SLA5850 BROOKS 0251

LabVIEW开发BROOKS SLA5850 BROOKS 0251 SLA5800 系列热式质量流量计和质量流量控制器在精度、稳定性和可靠性方面堪称标杆&#xff0c;因而得到广泛的认可。这些产品具有广泛的流量测量范围&#xff0c;适用于各种温度和压力条件&#xff0c;非常适合化工和石化研究、实验室、…

【Gradle】实现自动化构建和测试,提高代码质量和可靠性

做Android开发的同学&#xff0c;对Gradle肯定不陌生&#xff0c;我们用它配置、构建工程&#xff0c;可能还会开发插件来促进我们的开发&#xff0c;我们必须了解Gradle。Gradle是一种基于Groovy的项目自动化构建工具&#xff0c;用于编译、打包、测试、发布和依赖管理等任务。…

pdf可以转化成excel表格吗?四个方法可以帮助到你!

PDF文件是许多用户在工作中经常接触的文件格式。为了保持格式排版的一致性并防止篡改&#xff0c;在传输过程中通常会先将文件转换为PDF格式。然而&#xff0c;如果需要编辑其中的数据&#xff0c;就需要先将PDF转换为Excel才能继续相关操作。那么&#xff0c;如何将PDF转换为E…

腾讯云服务器CPU大全_处理器主频性能

腾讯云服务器CPU采用什么处理器型号&#xff1f;主频睿频多少&#xff1f;腾讯云服务器CPU性能如何&#xff1f;云服务器CVM规格不同CPU型号也不同&#xff0c;轻量应用服务器的CPU处理器性能如何&#xff1f;腾讯云服务器网分享腾讯云服务器CPU处理器大全&#xff1a; 目录 …

如何通过 CrossOver 在 Mac 苹果电脑上安装使用 windows 应用程序?

首先我们可以先了解一下什么是 crossOver &#xff0c;CrossOver 是 Mac 和 Windows 系统之间的兼容工具。使 Mac 操作系统的用户可以运行 Windows 系统的应用&#xff0c;从办公软件、实用工具、游戏到设计软件&#xff0c; 您都可以在 Mac 程序和 Windows 程序之间随意切换。…

csapp 深入理解计算机系統 笔记

csapp 深入理解计算机系統 笔记 参考lab 第1章&#xff1a;计算机系统漫游第 2 章&#xff1a;信息的表示和处理Data Lab 参考 计算机速成课 | Crash Course 字幕组 (全40集 2018-5-1 精校完成)csapp重点解读深入理解计算机系統 csapp lab Lab AssignmentsLab 直接下载参考 …

什么是深度学习的误差分解

误差分解是将深度学习模型的预测误差拆分为多个组成部分&#xff0c;以便更好地理解模型性能。在深度学习中&#xff0c;我们通常将预测误差分解为三个部分&#xff1a;偏差&#xff08;Bias&#xff09;、方差&#xff08;Variance&#xff09;和不可避免的误差&#xff08;Ir…

python离线安装ibm_db

下载离线包ibm_db以及clidriver 下载imb_db 在pypi官方网站https://pypi.org/project/ibm-db/#files下载离线安装包ibm_db-3.0.2.tar.gz。下载clidriver 下载地址&#xff1a;https://public.dhe.ibm.com/ibmdl/export/pub/software/data/db2/drivers/odbc_cli/nt32_odbc_cli.…

Jmeter场景设置与启动方式

目录 线程组设置 name和comments Action to be taken after a sampler error Thread Properties 线程属性设置 场景运行 远程运行配置 命令行 性能测试场景是用来模拟模拟真实用户操作的工作单元&#xff0c;所以场景设计一定要切合用户的操作逻辑&#xff0c;jmeter主要…

window安装MongoDB

安装直接先去官网下载 Download MongoDB Community Server | MongoDB 安装后如下&#xff0c;我们直接双击运行&#xff0c; 这里记得选下面(可以自己选择安装盘符位置)&#xff0c;上面第一个会自动帮你安装到C盘&#xff0c;然后选择下一步 &#xff0c;这里勾选就会选择去自…

Spring MVC数据校验

一般情况下&#xff0c;用户的输入是随意的&#xff0c;为了保证数据的合法性&#xff0c;数据验证是所有 Web 应用必须处理的问题。 Spring MVC 有以下两种方法可以验证输入&#xff1a; 1.利用 Spring 自带的验证框架(复杂&#xff0c;推荐spring-boot-starter-validation) …