Hikari源码分析

news2025/1/16 1:37:18

总结

连接池关系

1、HikariDataSource构建函数->生成HikariPool对象->调用HikariPool的getConection得到连接
2、HikariPool包含ConcurrentBag
3、ConcurrentBag保存连接:三个集合threadList、sharedList、handoffQueue
4、ConcurrentBag管理连接:创建连接的线程池,探活的线程池,关闭连接的线程池、阻塞队列
5、探活的线程池:调用关闭连接的线程池,调用创建链接的线程池。
6、连接Connection被包装成了poolEntry,通过poolEntryCreator->得到poolEntry->添加到shareList里面去。
7、释放链接:判断是否人等待获取链接的请求,如果有塞到handoffQueue,如果没有添加到自己线程的threadList里面去。

数据库连接池主要分为两大内容

1、连接的获取,主要涉及三个容器: threadList、sharedList、handoffQueue
2、连接池的管理:主要包含连接的创建,连接的定期检测,是否有效是否空闲,连接的关闭

在这里插入图片描述

源码分析

HikariDataSource常用的参数配置

connectionTimeou:客户端创建连接等待超时时间,如果30秒内没有获取连接则抛异常,不再继续等待
idleTimeout:连接允许最长空闲时间,如果连接空闲时间超过1分钟,则会被关闭
maxLifetime:连接最长生命周期,当连接存活时间达到30分钟之后会被关闭作退休处理
minimumIdle:连接池中最小空闲连接数
maximumPoolSize:连接池中最大连接数
validationTimeout:测试连接是否空闲的间隔
leadDetectionThreshold:连接被占用的超时时间,超过1分钟客户端没有释放连接则强制回收该连接,防止连接泄漏

1、HikariPool

Hikari中的核心类为HikariDataSource,表示Hikari连接池中的数据源,实现了DataSource接口的getConnection方法,获取链接主要是为了拿到Connection,而拿到Connection就是要通过HikariPool的实例对象来获取。

所以在HikariDataSource的构造函数里面会创建HikariPool对象。为了在调用getConnection方法时,从HikariPool里面拿到Connection

private final HikariPool fastPathPool;
private volatile HikariPool pool;

public HikariDataSource(HikariConfig configuration){
  pool = fastPathPool = new HikariPool(this);
}

/** 获取连接*/
public Connection getConnection() throws SQLException{
    if (fastPathPool != null) {
        return fastPathPool.getConnection();
    }
    pool = result = new HikariPool(this);
    /** 调用pool的getConnection()方法获取连接*/
    return result.getConnection();
}

小结:HikariDataSource->生成HikariPool对象->HikariPool对象得到Connection。

重点看HikariPool的getConnection()方法逻辑时如何得到connection

/** 获取连接*/
public Connection getConnection(final long hardTimeout) throws SQLException{
    /** 获取锁*/
    suspendResumeLock.acquire();

    try {

        do {
            /** 1、从ConcurrentBag中借出一个PoolEntry对象 */
            PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
            if (poolEntry == null) {
                break;
            }

            /** 判断连接是否被标记为抛弃 或者 空闲时间过长, 是的话就关闭连接*/
            if (......) {
                closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
            }
            
			/** 2、拿到poolEntry,通过Javassist创建代理连接*/
            return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);

        } while (timeout > 0L);

        throw createTimeoutException(startTime);
    } finally {
        /** 释放锁*/
        suspendResumeLock.release();
    }
}

核心步骤只有两步,一个是调用ConcurrentBag的borrow方法借用一个PoolEntry对象,第二步调用调用PoolEntry的createProxyConnection方法动态生成代理connection对象。
这里涉及到了两个核心的类,分别是ConcurrentBag和PoolEntry

2、ConcurrentBag

1、PoolEntry :PoolEntry顾名思义是连接池的节点,里面有个connection属性,实际也可以看作是一个Connection对象的封装,连接池中存储的连接就是以PoolEntry的方式进行存储。

2、ConcurrentBag:ConcurrentBag本质就是连接池的主体,存储对象PoolEntry(PoolEntry主要是封装了connection),另外做了并发控制来解决连接池的并发问题。里面有锁对象,缓存PoolEntry集合,sharedList、threadList等。

/** 借出一个对象 */
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
    /** 1.从ThreadLocal中获取当前线程绑定的对象集合 */
    final List<Object> list = threadList.get();
    /** 1.1.如果当前线程变量中存在就直接从list中返回一个*/
    for (int i = list.size() - 1; i >= 0; i--) {
        ......如果bagEntry不为空。尝试将bagEntry的状态从未被使用更新成已使用。成功返回PoolEntry
        if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
        }
    }

    /** 2.遍历当前缓存的sharedList, 如果当前状态为未使用,则通过CAS修改为已使用*/
    for (T bagEntry : sharedList) {
        if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
        }
    }

    do {
        /** 3.从阻塞队列中等待超时获取元素,如果获取元素失败或者获取元素且使用成功则均返回 */
        final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
        if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
        }
    } while (timeout > 10_000);//如果超时没有拿到PoolEntry,在再拿,应该有个最大超市次数。

    return null;
}

1、从ThreadLocal中获取或者说从threadList里面获取。注意一般存在ThreadLocal的数据都是弱引用类型。

ThreadLocal里面的PoolEntry什么时候存进去的?没有人使用时那么将连接存入ThreadLocal中,每个ThreadLocal最多会存储50个连接

2、从sharedList中获取:ThreadLocal中获取连接失败之后,会再次尝试从sharedList中获取

sharedList的PoolEntry是什么时候存进去的?
在ConcurrentBag初始化的,会初始化指定数量的PoolEntry对象存入sharedList

3、handoffQueue从阻塞队列获取

什么时候会有连接往里面塞?

其实在创建ConcurrentBag的对象的时候,他的构造函数,已经对这三个集合对象初始化,但是并没有塞数据进去。

public ConcurrentBag(...){

    //1、threadList
    if (是否为弱引用) {
        this.threadList = ThreadLocal.withInitial(() -> new ArrayList<>(16));
    } else {
        this.threadList = ThreadLocal.withInitial(() -> new FastList<>(IConcurrentBagEntry.class, 16));
    }

    //2、初始化sharedList
    this.sharedList = new CopyOnWriteArrayList<>();

    //3、初始化阻塞队列handoffQueue
    this.handoffQueue = new SynchronousQueue<>(true);
}

小结:borrow方法从下面三个对象中获取

1、threadList
2、sharedList    
3、handoffQueue  指定超时时间,没有拿到结束请求。

3、管理【连接池】

HikariPool内部属性包含了ConcurrentBag对象,在HikariPool初始化时会创建ConcurrentBag对象,所以ConcurrentBag的构造函数是在HikariPool初始化时调用,HikariPool构造函数如下:

public HikariPool(final HikariConfig config) {
    //1、初始化ConcurrentBag对象
    this.connectionBag = new ConcurrentBag<>(this);

	/** 2、【创建连接】的线程池*/
    this.addConnectionExecutor = createThreadPoolExecutor(addQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardPolicy());

    /** 3、【探活】的线程池,通过定时检测,保持固定数量的"有效连接", 怎么的Connection属于有效? */
    this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
	this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService);

	/**【保持连接池连接数量】的任务,通过【探活】线程池来执行的,执行任务【HouseKeeper】,houseKeeperTask只是执行的返回结果,重点在于提交的任务类【HouseKeeper】 */
    this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);


    /** 4、【关闭连接】的线程池 */
    this.closeConnectionExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), "xx", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());

    /** 5、【阻塞队列】根据配置的最大连接数, 当连接数不够时,需要获取链接的线程就在这里等待 */
    LinkedBlockingQueue<Runnable> addQueue = new LinkedBlockingQueue<>(config.getMaximumPoolSize());
    this.addConnectionQueue = unmodifiableCollection(addQueue);   
}

这里有一个定时任务houseKeeperTask,该定时任务的作用是定时检测【连接池中的连接】,保持连接池中的【连接数】稳定在一个固定的值,执行的内容就是HouseKeep的run方法

private final class HouseKeeper implements Runnable {
    @Override
    public void run(){

    	/** 关闭连接池中需要【被丢弃】的连接,每个链接有个【最大生命周期】,超过这个周期都视为被丢弃的连接 */
        if (plusMillis(now, 128) < plusMillis(previous, housekeepingPeriodMs)) {
            softEvictConnections();
            return;
        }
   

        /** 获取当前连接池中【不是使用中】的连接集合 */            
        final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE);
        int toRemove = notInUse.size() - config.getMinimumIdle();
        for (PoolEntry entry : notInUse) {
            /** 当前空闲的连接如果超过【最大空闲时间idleTimeout】则关闭空闲连接 */
            if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
                closeConnection(entry, "(connection has passed idleTimeout)");
                toRemove--;
            }
        }

        /** 填充连接池,保持连接池数量至少保持【minimum个连接】数量 */
        fillPool(); // Try to maintain minimum connections
    }
}

最大生命周期和空闲时间是2个概念。

该定时任务主要是为了维护连接池中连接的数量,首先需要将被标记为需要丢弃的连接进行关闭,然后将空闲超时的连接进行关闭,最后当连接池中的连接少于最小值时就需要对连接池进行补充连接的操作。所以在初始化连接池时,初始化连接的操作就是在fillPool方法中实现的。fillPool方法源码如下:

/** 填充连接池 */
private synchronized void fillPool() {
    //计算需要添加的连接数量
    final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections())
            - addConnectionQueue.size();
    for (int i = 0; i < connectionsToAdd; i++) {
        /** 向【创建连接】的线程池,提交创建连接的任务 */
        addConnectionExecutor.submit(poolEntryCreator);
    }
}

先计算需要创建的连接数量,向创建连接的线程池中提交任务 poolEntryCreator,PoolEntryCreator创建PoolEntry对象的逻辑如下:

/** 创建PoolEntry对象线程 */

private final class PoolEntryCreator implements Callable<Boolean> {
    @Override
    public Boolean call() {
        /** 1.当前连接池状态正常并且需求创建连接时 */
        while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) {
            /** 2.创建PoolEntry对象 */
            final PoolEntry poolEntry = createPoolEntry();
            if (poolEntry != null) {
                /** 3.将PoolEntry对象添加到ConcurrentBag对象中的sharedList中 */
                connectionBag.add(poolEntry);
                return Boolean.TRUE;
            }
            /** 如果创建失败,睡眠指定时间,回到循环在尝试创建 */
            quietlySleep(sleepBackoff);
        }
        return Boolean.FALSE;
    }
}

createPoolEntry方法逻辑如下:

/** 创建PoolEntry对象 */
private PoolEntry createPoolEntry() {
    /** 1.初始化PoolEntry对象,newPoolEntry里面会先创建Connection对象,PoolEntry再把Connection封装一下返回 */
    final PoolEntry poolEntry = newPoolEntry();
    /** 获取连接最大生命周期时长 */
    final long maxLifetime = config.getMaxLifetime();


    if (maxLifetime > 0) {
        /** 2.给PoolEntry添加定时任务,当PoolEntry对象达到最大生命周期时间后触发定时任务将连接标记为被抛弃 */
        poolEntry.setFutureEol(houseKeepingExecutorService.schedule(() -> {
                    /** 判断刚创建的连接poolEntry,是否达到最大生命周期,如果满足则抛弃连接 */
                    if (softEvictConnection(poolEntry, "xx", false)) {
                        /** 丢弃一个连接之后,调用addBagItem补充新的PoolEntry对象 */
                        addBagItem(connectionBag.getWaitingThreadCount());
                    }
                }, lifetime, MILLISECONDS));
    }

    return poolEntry;
}

PoolEntry newPoolEntry() throws Exception{
  return new PoolEntry(newConnection(), this, isReadOnly, isAutoCommit);
}

首先创建一个新的PoolEntry对象,PoolEntry构造时会创建Connection对象,另外如果连接设置了最大生命周期时长,那么需要给每个PoolEntry添加定时任务,为了防止多个PoolEntry同时创建同时被关闭,所以每个PoolEntry的最大生命周期时间都不一样。当PoolEntry达到最大生命周期后会触发softEvictConnection方法,将PoolEntry标记为需要被丢弃,另外由于抛弃了PoolEntry对象,所以需要重新调用addBagItem方法对PoolEntry对象进行补充。

上面会调用addBagItem方法,我们看下addBagItem的做了什么?

public void addBagItem(final int waiting) {
    /** 判断是否需要创建连接 */
    final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Yes, >= is intentional.
    if (shouldAdd) {
        /** 向创建连接线程池中提交创建连接的任务 */
        addConnectionExecutor.submit(poolEntryCreator);
    }
}

总结:

从ConcurrentBag中获取连接一共分成三步,首先从当前线程的ThreadLocal中获取,如果有直接返回一个连接,如果ThreadLocal中没有则从sharedList中获取,sharedList可以理解为ConcurrentBag缓存的连接池,每当创建了一个PoolEntry对象之后都会添加到sharedList中去,如果sharedList中的连接状态都不是可用状态,此时就需要通过IBagStateListener提交一个创建连接的任务,交给创建连接的线程池去执行,创建新的连接。

新的连接创建成功之后会将PoolEntry对象添加到无容量的阻塞队列handoffQueue中,当请求线程进来,如果ThreadLocal和sharedList都没有拿到,就不断尝试从handoffQueue队列中获取连接直到成功获取或者超时返回。

HikariPool包含->ConcurrentBag->包含threadList、sharedList、handoffQueue

释放连接

当客户端释放连接时会调用collection的close方法,Hikari中的Connection使用的是代理连接ProxyConnection对象,调用close方法时会调用关联的PoolEntry对象的回收方法recycle方法,PoolEntry的recycle方法源码如下,主要还是通过hikariPool拿到connectionBag,嗲用connectionBag的requite方法。

void recycle(final long lastAccessed) {
    if (connection != null) {
        hikariPool.recycle(this);
    }
}
/** 调用HikariPool的recycle方法,回收当前PoolEntry对象 */
void recycle(final PoolEntry poolEntry) {
    /** 调用ConcurrentBag的回收方法 */
    connectionBag.requite(poolEntry);
}

/** 回收元素方法 */
public void requite(final T bagEntry) {
    /** 1.设置状态为未使用 */
    bagEntry.setState(STATE_NOT_IN_USE);

    /** 2.如果当前存在等待线程,则优先将元素给等待线程 */
    for (int i = 0; waiters.get() > 0; i++) {
        /** 2.1.将元素添加到无界阻塞队列中,等待其他线程获取 */
        if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
            return;
        } else {
            /** 当前线程不再继续执行,让出CPU执行权 */
            yield();
        }
    }

    /** 3.如果当前连接没有被其他线程使用,则添加到当前线程的ThreadLocal中,当这个线程再次执行DB操作时,会优先从ThreadLocal里面拿 */
    final List<Object> threadLocalList = threadList.get();
    if (threadLocalList.size() < 50) {
        threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
    }
}

回收连接最终会调用ConcurrentBag的requite方法,方法逻辑不复杂,首先将PoolEntry元素状态设置为未使用,然后判断当前是否存在等待连接的线程,如果存在则将连接加入到无界阻塞队列中去,由等待连接的线程从阻塞队列中去获取;

如果当前没有等待连接的线程,则将连接添加到本地线程变量ThreadLocal中,等待当前线程下次获取连接时直接从ThreadLocal中获取。

源码分析:跳转

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

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

相关文章

maven:编译出现Process terminated解决方法(超全)

maven:编译出现Process terminated解决方法&#xff08;超全&#xff09; 一. 情况一&#xff1a;配置文件 settings. xml 出错&#xff08;解决方法1&#xff09;1.1 项目编译报错如下&#xff1a;1.2 点击【项目名】提示找到出错文件1.3 点击查看出错文件1.4 原因及解决办法 …

LeetCode 2401.最长优雅子数组 ----双指针+位运算

数据范围1e5 考虑nlog 或者n的解法&#xff0c;考虑双指针 因为这里要求的是一段连续的数组 想起我们的最长不重复连续子序列 然后结合一下位运算就好了 是一道双指针不错的题目 class Solution { public:int longestNiceSubarray(vector<int>& nums) {int n nums…

微信朋友圈如何关闭?

怎样关闭微信朋友圈&#xff1f;由于一些比较特殊的原因&#xff0c;有些人不想再发朋友圈了&#xff0c;或者想跟过去的自己说“拜拜”&#xff0c;所以就想把微信朋友圈给关掉。虽然这种需求的人并不多&#xff0c;但却真实存在着。 微信早期版本是有关闭朋友圈开关的&#x…

【C# Programming】委托和lambda表达式、事件

目录 一、委托和lambda表达式 1.1 委托概述 1.2 委托类型的声明 1.3 委托的实例化 1.4 委托的内部机制 1.5 Lambda 表达式 1.6 语句lambda 1.7 表达式lambda 1.8 Lambda表达式 1.9 通用的委托 1.10 委托没有结构相等性 1.11 Lambda表达式和匿名方法的内部机制 1.1…

vite+vue3路由切换滚动条位置重置el-scrollbar

vitevue3路由切换滚动条位置重置 本文目录 vitevue3路由切换滚动条位置重置使用原生滚动条使用el-scrollbaruseRoute和useRouter 当切换到新路由时&#xff0c;想要页面滚到顶部&#xff0c;或者是保持原先的滚动位置&#xff0c;就像重新加载页面那样&#xff0c;vue-router 可…

升级版运算放大器应用电路(二)

网友&#xff1a;你上次分享的经典运算放大电路太棒了&#xff0c;再分享几个吧&#xff01; 工程师&#xff1a;好吧&#xff0c;那你瞪大耳朵&#xff0c;看好了~ 1、仪器放大电路&#xff0c;此电路使用于小信号的放大&#xff0c;一般用于传感器信号的放大。传感器的输出信…

紫光同创PGL50H图像Sobel边缘检测

本原创文章由深圳市小眼睛科技有限公司创作&#xff0c;版权归本公司所有&#xff0c;如需转载&#xff0c;需授权并注明出处 适用于板卡型号&#xff1a; 紫光同创PGL50H开发平台&#xff08;盘古50K开发板&#xff09; 一&#xff1a;软硬件平台 软件平台&#xff1a;PDS_…

ROS中如何实现将一个基于A坐标系下的三维向量变换到基于B坐标系下?

摘要 ROS中通过tf.TransformListener.lookupTransform方法获取从A坐标系到B坐标系的旋转四元数rot&#xff0c;通过quaternion_multiply函数实现 p ′ q p q − 1 p qpq^{-1} p′qpq−1中的矩阵乘法&#xff0c;从而可以方便获取在新坐标系下的四元数坐标表示 p ′ p p′. 基…

app广告变现,开发者如何提高用户的参与度?

越来越多的开发者已经认识到用户参与移动应用内广告的价值。 1、人性化的消息传递 人性化的消息传递在情感层面上与用户产生共鸣的方式进行沟通&#xff0c;让他们感到被理解、被重视和参与。它在用户之间建立了一种信任感和可信度。对话式和相关的语气建立了超越交易关系的联…

OBS直播软件使用NDI协议输入输出

OBS&#xff08;Open Broadcaster Software&#xff09;是一个免费的开源的视频录制和视频推流软件。其功能强大并广泛使用在视频导播、录制及直播等领域。 OBS可以导入多种素材&#xff0c;除了本地音频、视频、图像外&#xff0c;还支持硬件采集设备&#xff0c;更能支持各种…

致远OA wpsAssistServlet任意文件读取漏洞复现 [附POC]

文章目录 致远OA wpsAssistServlet任意文件读取漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现 0x06 修复建议 致远OA wpsAssistServlet任意文件读取漏洞复现 [附POC] 0x01 前言 免责声明&#xff1a;请勿利用…

多测师肖sir_高级金牌讲师_性能测试之badboy录制脚本02

性能测试之badboy录制脚本 一、下载安装包&#xff0c;点击安装 二、点击我同意 三、选择路径&#xff0c;点击install 打开以下界面&#xff0c;表示安装成功 第二步&#xff1a;录制流程 界面视图&#xff0c;模拟浏览器&#xff0c;能够进行操作 需要录制脚本的URL 点…

英语小作文模板(06求助+描述;07描述+建议)

06 求助描述&#xff1a; 题目背景及要求 第一段 第二段 第三段 翻译成中文 07 描述&#xff0b;建议&#xff1a; 题目背景及要求 第一段 第二段

记一次 AWD 比赛中曲折的 Linux 提权

前提背景&#xff1a; 今天一场 AWD 比赛中&#xff0c;遇到一个场景&#xff1a;PHP网站存在SQL注入和文件上传漏洞, MYSQL当前用户为ROOT&#xff0c;文件上传蚁剑连接SHELL是权限很低的用户。我需要想办法进行提权&#xff0c;才能读取到 /root 目录下的 flag。 一、sqlmap …

通过外网客户端远程到内部区域网环境

拓扑 项目需要远程内部区域网德服务器&#xff0c;可以提供一台双网卡的电脑来实现

面向制造企业的持续发展,2023数字化工单管理系统创新篇章-亿发

面向制造企业的持续发展&#xff0c;2023数字化工单管理系统开创新篇章-亿发 随着制造业的持续发展&#xff0c;运维工单管理日益成为关键环节&#xff0c;它设计客户管理、设备维护、服务商合作等多个业务领域&#xff0c;对运营效率和服务质量有着重要影响。然而&#xff0c…

MySQL用户管理和授权

用户管理和授权是属于MySQL当中的DCL语句 创建用户以及一些相关操作 明文创建用户 create user zzrlocalhost IDENTIFIED by 123456;create user 这是创建用户的开头zzr表示用户名 localhost&#xff1a;新建的用户可以在哪些主机上登录。即可以使用IP地址&#xff0c;网段&a…

竞赛选题 深度学习图像风格迁移 - opencv python

文章目录 0 前言1 VGG网络2 风格迁移3 内容损失4 风格损失5 主代码实现6 迁移模型实现7 效果展示8 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习图像风格迁移 - opencv python 该项目较为新颖&#xff0c;适合作为竞赛课题…

SAP MM学习笔记39 - MRP(资材所要量计划)

这一章开始&#xff0c;离开请求书&#xff0c;学点儿新知识啦。 MRP ( Material Requirement Planning ) - 资材所要量计划。 它的位置在下面的调达周期图上来看&#xff0c;就是右上角的 所要量决定那块儿。 1&#xff0c;MRP(资材所要量计划) 的概要 MRP 的主要目的就是 确…

2024云渲染渲染100超简便的使用方法!渲染100云渲染邀请码5858

云渲染解决了本地电脑只能同时渲染一张图&#xff0c;并且占用本地电脑情况&#xff0c;让云渲染使用者也越来越多&#xff01; 最近好多朋友在问我渲染100 - 官方注册邀请码【5858】如何提交渲染&#xff1f;今天我来总结一下 1.先在官网下载客户端&#xff0c;网页认证为渲染…