时间轮原理及其在框架中的系统设计
发布时间:2024-12-25
比如说先来看一下Dubbo中的的间隔时间轮的结构,可以看到,它和计时器很像,它被还包括变成了一个个Bucket,每个Bucket有一个头表头和叉表头,分别朝向双向数据流的头结点和叉结点,双向数据流中的存储的就是要三执行的侦查。间隔时间轮不停翻改投,当朝向Bucket0所督导维护的双向数据流时,就将它所存储的侦查给定放到来三执行。
比如说我们先来参阅下Dubbo中的间隔时间轮HashedWheelTimer所涉及到的一些整体概念,在宣讲完这些整体概念之后,再来对间隔时间轮的程式库进行时统计分析。
2.1 TimerTask在Dubbo中的,TimerTask积体电路了要分派的侦查,它就是上布双向数据流中的结点所积体电路的侦查。所有的除此以外侦查都所需继承TimerTask端口。如下布,可以看到Dubbo中的的腹痛侦查HeartBeatTask、注册败北依此类推侦查FailRegisteredTask等都借助了TimerTask端口。
public interface TimerTask { void run(Timeout timeout) throws Exception;} 2.2 TimeoutTimerTask中的run作法的退参是Timeout,Timeout与TimerTask一一并不相同,Timeout的唯一借助类HashedWheelTimeout中的就积体电路了TimerTask特性,可以理解为HashedWheelTimeout就是上述双向数据流的一个结点,因此它也还包括两个HashedWheelTimeout类型的表头,分别朝向近期结点的上一个结点和下一个结点。
public interface Timeout { // Timer就是除此以外器, 也就是Dubbo中的的间隔时间轮 Timer timer(); // 赚取该结点要分派的侦查 TimerTask task(); // 确实该结点积体电路的侦查不对过期、被中止 boolean isExpired(); boolean isCancelled(); // 中止该结点的侦查 boolean cancel();}HashedWheelTimeout是Timeout的唯一借助,它的作用有两个:
它是间隔时间轮底部所维护的双向数据流的结点,其中的积体电路了单单要分派的侦查TimerTask。通过它可以查看除此以外侦查的稳定状态、对除此以外侦查进行时中止、从双向数据流中的去掉等可用。比如说来看一下Timeout的借助类HashedWheelTimeout的整体codice_与借助。
1) int ST_INIT = 0、int ST_CANCELLED = 1、int ST_EXPIRED = 2 HashedWheelTimeout里界定了三种稳定状态,分别暗示侦查的codice_时稳定状态、被中止稳定状态、已过期稳定状态 2) STATE_UPDATER 用作更新除此以外侦查的稳定状态 3) HashedWheelTimer timer 朝向间隔时间轮对象 4) TimerTask task 单单要分派的侦查 5) long deadline 指除此以外侦查分派的间隔时间,这个间隔时间是在创始人 HashedWheelTimeout 时自行最终的 算出公式是: currentTime(创始人 HashedWheelTimeout 的间隔时间) + delay(侦查延迟间隔时间) - startTime(HashedWheelTimer 的重启间隔时间),间隔时间单位为大概 6) int state = ST_INIT 侦查初始稳定状态 7) long remainingRounds 指近期侦查剩下的计时器短周期数. 间隔时间轮所能暗示的间隔时间长度是有限的, 在侦查届满间隔时间与近期时刻 的间隔偏移少于间隔时间轮创纪录能暗示的不间断,就显现出了套圈的原因,所需该codice_值暗示剩下的计时器短周期 8) HashedWheelTimeout next、HashedWheelTimeout prev 分别并不相同近期除此以外侦查在数据流中的的前驱结点和后继结点,这也验证了间隔时间轮中的每个底部所并不相同的侦查数据流是 一个双数据流 9) HashedWheelBucket bucket 间隔时间轮中的的一个底部,并不相同间隔时间轮圆圈的一个个小格子,每个底部维护一个双向数据流,当间隔时间轮表头改投到近期 底部时,就不会从底部所督导的双向数据流中的放到侦查进行时三执行HashedWheelTimeout赚取了remove可用,可以从双向数据流中的去掉近期自身结点,并将近期间隔时间轮所维护的除此以外侦查适用量减一。
void remove() { // 赚取近期侦查属于哪个底部 HashedWheelBucket bucket = this.bucket; if (bucket != null) { // 从底部中的去掉自己,也就在在双向数据流中的去掉结点, // 统计分析bucket的作法时不会统计分析 bucket.remove(this); } else { // pendingTimeouts暗示近期间隔时间轮所维护的除此以外侦查的适用量 timer.pendingTimeouts.decrementAndGet(); }}HashedWheelTimeout赚取了cancel可用,可以中止间隔时间轮中的的除此以外侦查。当除此以外侦查被中止时,它不会首先被移出到canceledTimeouts数据流中的。在间隔时间轮翻改投到底部进行时侦查三执行此前和间隔时间轮放弃行驶时都不会codice_cancel,而cancel不会codice_remove,从而挖掘该数据流中的被中止的除此以外侦查。
@Overridepublic boolean cancel() { // 通过CAS进行时稳定状态变更 if (!compareAndSetState(ST_INIT, ST_CANCELLED)) { return false; } // 侦查被中止时,间隔时间轮不会将它移出到间隔时间轮所维护的canceledTimeouts数据流中的. // 在间隔时间轮翻改投到底部进行时侦查三执行此前和间隔时间轮放弃行驶时都不会codice_cancel,而 // cancel不会codice_remove,从而挖掘该数据流中的被中止的除此以外侦查 timer.cancelledTimeouts.add(this); return true;}HashedWheelTimeout赚取了expire可用,当间隔时间轮表头翻改投到某个底部时,不会给定该底部所维护的双向数据流,确实结点的稳定状态,如果辨认出侦查已届满,不会通过remove作法去掉,然后codice_expire作法分派该除此以外侦查。
public void expire() { // 修正除此以外侦查稳定状态为已过期 if (!compareAndSetState(ST_INIT, ST_EXPIRED)) { return; } try { // 或许的分派除此以外侦查所要代表人的范式 task.run(this); } catch (Throwable t) { // 打印日志,可以看到当间隔时间轮中的除此以外侦查分派极其时, // 不不会推退出极其,影响到间隔时间轮中的其他除此以外侦查分派 }}2.3 HashedWheelBucket右边也参阅过了,它是间隔时间轮中的的底部,它实质上维护了双向数据流的字字表头。比如说我们来看一下它实质上的整体资源和借助。
1) HashedWheelTimeout head、HashedWheelTimeout tail 朝向该底部所维护的双向数据流的首结点和叉结点HashedWheelBucket赚取了addTimeout作法,用作添加侦查到双向数据流的叉结点。
void addTimeout(HashedWheelTimeout timeout) { // 添加此前确实一下该侦查近期没有被被相似之处到一个底部上 assert timeout.bucket == null; timeout.bucket = this; if (head == null) { head = tail = timeout; } else { tail.next = timeout; timeout.prev = tail; tail = timeout; }}HashedWheelBucket赚取了remove作法,用作从双向数据流中的截图自行最终结点。整体范式如下布附注,根据要截图的结点辨认出其四轮驱动结点和后置结点,然后分别变更四轮驱动结点的next表头和后置结点的prev表头。截图过程中的所需考虑一些边界线原因。截图之后将pendingTimeouts,也就是近期间隔时间轮的待三执行侦查数减一。remove代码范式较直观,这边就不贴代码了。
HashedWheelBucket赚取了expireTimeouts作法,当间隔时间轮表头翻改投到某个底部时,通过该作法三执行该底部上双向数据流的除此以外侦查,还包括3种原因:
除此以外侦查已届满,则不会通过remove作法放到,并codice_其expire作法分派侦查范式。除此以外侦查已被中止,则通过remove作法放到直接扔掉。除此以外侦查还没届满,则不会将remainingRounds(剩下计时器短周期)减一。void expireTimeouts(long deadline) { HashedWheelTimeout timeout = head; // 间隔时间轮表头改投到某个底部时从双向数据流头结点开始给定 while (timeout != null) { HashedWheelTimeout next = timeout.next; // remainingRounds <= 0暗示届满了 if (timeout.remainingRounds <= 0) { // 从数据流中的去掉该结点 next = remove(timeout); // 确实该除此以外侦查确实是届满了 if (timeout.deadline <= deadline) { // 分派该侦查 timeout.expire(); } else { // 推退极其 } } else if (timeout.isCancelled()) { // 侦查被中止,去掉后直接扔掉 next = remove(timeout); } else { // 剩下计时器短周期减一 timeout.remainingRounds--; } // 继续确实下一个侦查结点 timeout = next; }}HashedWheelBucket也赚取了clearTimeouts作法,该作法不会在间隔时间轮暂停的时候被适用,它不会给定并去掉所有双向数据流中的的结点,并来到所有没加班和没被中止的侦查。
2.4 WorkerWorker借助了Runnable端口,间隔时间轮实质上通过Worker文件系统来三执行放到间隔时间轮中的的除此以外侦查。比如说先来看一下它的整体codice_和run作法范式。
1) Set unprocessedTimeouts 当间隔时间轮暂停时,用作储藏间隔时间轮中的没过期的和没被中止的侦查 2) long tick 间隔时间轮表头,朝向间隔时间轮中的某个底部,当间隔时间轮翻改投时该tick不会自增 public void run() { // codice_时startTime, 所有侦查的的deadline都是相对于这个间隔时间点 startTime = System.nanoTime(); // 唤起堵塞在start()的文件系统 startTimeInitialized.countDown(); // 只要间隔时间轮的稳定状态为WORKER_STATE_STARTED, 就周而复始的翻改投tick, // 三执行底部中的的除此以外侦查 do { // 确实是否到了三执行底部的间隔时间了,还到时则sleep一不会 final long deadline = waitForNextTick(); if (deadline> 0) { // 赚取tick并不相同的底部索引 int idx = (int) (tick Co mask); // 挖掘使用者主动中止的除此以外侦查, 这些除此以外侦查在使用者中止时, // 不会记录到 cancelledTimeouts 数据流中的. 在每次表头翻改投 // 的时候,间隔时间轮都不会挖掘该数据流 processCancelledTasks(); // 根据近期表头定位并不相同底部 HashedWheelBucket bucket = wheel[idx]; // 将文件系统在 timeouts 数据流中的的除此以外侦查集中于到间隔时间轮中的并不相同的底部中的 transferTimeoutsToBuckets(); // 三执行该底部位的双向数据流中的的除此以外侦查 bucket.expireTimeouts(deadline); tick++; } // 探测间隔时间轮的稳定状态, 如果间隔时间轮三处于行驶稳定状态, 则周而复始分派上述工序, // 慢慢分派除此以外侦查 } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED); // 这里应该是间隔时间轮暂停了, 拔除所有底部中的的侦查, 并改投至到没三执行侦查列表, // 以供stop()作法来到 for (HashedWheelBucket bucket : wheel) { bucket.clearTimeouts(unprocessedTimeouts); } // 将还没有改投至到底部中的的待三执行除此以外侦查数据流中的的侦查放到, 如果是没中止 // 的侦查, 则改投至到没三执行侦查数据流中的, 以供stop()作法来到 for (; ; ) { HashedWheelTimeout timeout = timeouts.poll(); if (timeout == null) { break; } if (!timeout.isCancelled()) { unprocessedTimeouts.add(timeout); } } // 再一再次挖掘 cancelledTimeouts 数据流中的使用者主动中止的除此以外侦查 processCancelledTasks();}比如说对run作法中的涉及到的一些作法进行时参阅:
1)waitForNextTick范式比较直观,它不会确实不对到达三执行下一个底部侦查的间隔时间了,如果还没有到达则sleep一不会。
2)processCancelledTasks给定cancelledTimeouts,赚取被中止的侦查并从双向数据流中的去掉。
private void processCancelledTasks() { for (; ; ) { HashedWheelTimeout timeout = cancelledTimeouts.poll(); if (timeout == null) { // all processed break; } timeout.remove(); }}3)transferTimeoutsToBuckets当codice_newTimeout作法时,不会先将要三执行的侦查文件系统到timeouts数据流中的,等间隔时间轮表头翻改投时统一codice_transferTimeoutsToBuckets作法三执行,将侦查集中于到自行最终的底部并不相同的双向数据流中的,每次集中于10万个,以免堵塞间隔时间轮文件系统。
private void transferTimeoutsToBuckets() { // 每次tick只三执行10w个侦查, 以免堵塞worker文件系统 for (int i = 0; i < 100000; i++) { HashedWheelTimeout timeout = timeouts.poll(); // 没有侦查了直接跳出周而复始 if (timeout == null) { // all processed break; } // 还没有放到到底部中的就中止了, 直接则有 if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) { continue; } // 算出侦查所需经过多少个tick long calculated = timeout.deadline / tickDuration; // 算出侦查的轮数 timeout.remainingRounds = (calculated - tick) / wheel.length; // 如果侦查在timeouts数据流里面放久了, 以至于不太可能过了分派间隔时间, 这个时候 // 就适用近期tick, 也就是置放近期bucket, 此作法codice_完后就不会被分派. final long ticks = Math.max(calculated, tick); int stopIndex = (int) (ticks Co mask); // 将侦查改投至到相应的底部中的 HashedWheelBucket bucket = wheel[stopIndex]; bucket.addTimeout(timeout); }}2.5 HashedWheelTimer再一,我们来统计分析间隔时间轮HashedWheelTimer,它借助了Timer端口,赚取了newTimeout作法可以向间隔时间轮中的添加除此以外侦查,该侦查不会先被移出到timeouts数据流中的,等间隔时间轮翻改投到某个底部时,不会将该timeouts数据流中的的侦查集中于到某个底部所督导的双向数据流中的。它还赚取了stop作法用作终止间隔时间轮,该作法不会来到间隔时间轮中的没三执行的侦查。它也赚取了isStop作法用作确实间隔时间轮是否终止了。
先来看一下HashedWheelTimer的整体codice_。
1) HashedWheelBucket[] wheel 该数组就是间隔时间轮的环形数据流,数组每个元素都是一个底部,一个底部督导维护一个双向数据流,用作存储除此以外 侦查。它不会被在类型改投换中的codice_时,当自行最终为n时,它单单上不会取格特n的且为2的数列正数值。 2) Queue timeouts timeouts用作文件系统从外部向间隔时间轮草拟的除此以外侦查 3) Queue cancelledTimeouts cancelledTimeouts用作移出被中止的除此以外侦查,间隔时间轮不会在三执行底部督导的双向数据流此前,先三执行这两 个数据流中的的数据。 4) Worker worker 间隔时间轮三执行除此以外侦查的范式 5) Thread workerThread 间隔时间轮三执行除此以外侦查的文件系统 6) AtomicLong pendingTimeouts 间隔时间轮剩下的待三执行的除此以外侦查适用量 7) long tickDuration 间隔时间轮每个底部所代表人的间隔时间长度 8) int workerState 间隔时间轮稳定状态,可选值有init、started、shut down比如说来看一下间隔时间轮的类型改投换,用作codice_时一个间隔时间轮。首先它不会对传退参数ticksPerWheel进行时反改投三执行,来到大于该值的2的数列正数,它暗示间隔时间轮上有多少个底部,匹配是512个。然后创始人大小为该值的HashedWheelBucket[]数组。接着通过传退的tickDuration对间隔时间轮的tickDuration变量,匹配是100ms。节通过threadFactory创始人workerThread工作文件系统,该文件系统就是督导三执行间隔时间轮中的的除此以外侦查的文件系统。
public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit, int ticksPerWheel, long maxPendingTimeouts) { // 交叉路口上一共有多少个间隔时间间隔, HashedWheelTimer对其埃尔米特化时 // 将其折算为大于等于该值的2^n wheel = createWheel(ticksPerWheel); // 这用来快速算出侦查应该呆的底部 mask = wheel.length - 1; // 间隔时间轮每个底部的间隔时间间隔 this.tickDuration = unit.toNanos(tickDuration); // threadFactory是创始人文件系统的文件系统工厂对象 workerThread = threadFactory.newThread(worker); // 最多受限制多少个侦查等待分派 this.maxPendingTimeouts = maxPendingTimeouts;} private static HashedWheelBucket[] createWheel(int ticksPerWheel) { // 算出或许应当创始人多少个底部 ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel); // codice_时间隔时间轮数组 HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel]; for (int i = 0; i < wheel.length; i++) { wheel[i] = new HashedWheelBucket(); } return wheel;}codice_时间隔时间轮之后,就可以向其中的草拟除此以外侦查了,可以通过间隔时间轮赚取的newTimeout作法来进行时。首先将待三执行的侦查适用量加1,然后重启间隔时间轮文件系统,这时worker的run作法就不会被系统适时行驶。然后将该除此以外侦查积体电路变成HashedWheelTimeout改投至到timeouts数据流中的。start之后,间隔时间轮就开始行驶起来了,直到外界codice_stop作法终止放弃。
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) { // 待三执行的侦查适用量加1 long pendingTimeoutsCount = pendingTimeouts.incrementAndGet(); // 重启间隔时间轮 start(); // 算出该除此以外侦查的deadline long deadline = System.nanoTime() + unit.toNanos(delay) - startTime; // 创始人一个HashedWheelTimeout对象,它首先不会被移出到timeouts数据流中的 HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline); timeouts.add(timeout); return timeout;}public void start() { /** * 确实近期间隔时间轮的稳定状态 * 1) 如果是codice_时, 则重启worker文件系统, 重启整个间隔时间轮 * 2) 如果不太可能重启则则有 * 3) 如果是不太可能暂停,则报错 */ switch (WORKER_STATE_UPDATER.get(this)) { case WORKER_STATE_INIT: // 适用cas来确实重启间隔时间轮 if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) { workerThread.start(); } break; case WORKER_STATE_STARTED: break; case WORKER_STATE_SHUTDOWN: // 推退极其 default: throw new Error("Invalid WorkerState"); } // 等待worker文件系统codice_时间隔时间轮的重启间隔时间 while (startTime == 0) { try { // 这里适用countDownLatch来保证适时的文件系统不太可能被重启 startTimeInitialized.await(); } catch (InterruptedException ignore) { // Ignore - it will be ready very soon. } }}三、间隔时间轮应用到这里,Dubbo中的的间隔时间轮法则就统计分析回来。整整呼应本文开头的三个则有子,联结它们来统计分析下间隔时间轮在Dubbo或Redisson中的是如何适用的。
1)HeartbeatTimerTask在Dubbo的HeaderExchangeClient类中的不会向间隔时间轮中的草拟该腹痛侦查。
private void startHeartBeatTask(URL url) { // Client的具体借助最终是否重启该腹痛侦查 if (!client.canHandleIdle()) { AbstractTimerTask.ChannelProvider cp = () -> Collections.singletonList(HeaderExchangeClient.this); // 算出腹痛间隔, 最小间隔不能最低1s int heartbeat = getHeartbeat(url); long heartbeatTick = calculateLeastDuration(heartbeat); // 创始人腹痛侦查 this.heartBeatTimerTask = new HeartbeatTimerTask(cp, heartbeatTick, heartbeat); // 草拟到IDLE_CHECK_TIMER这个间隔时间轮中的等待分派, 等间隔时间到了间隔时间轮就不会去放到该侦查进行时适时分派 IDLE_CHECK_TIMER.newTimeout(heartBeatTimerTask, heartbeatTick, TimeUnit.MILLISECONDS); }}// 上面加进的IDLE_CHECK_TIMER就是我们本文的统计分析的间隔时间轮private static final HashedWheelTimer IDLE_CHECK_TIMER = new HashedWheelTimer(new NamedThreadFactory("dubbo-client-idleCheck", true), 1, TimeUnit.SECONDS, TICKS_PER_WHEEL);// 上述创始人腹痛侦查时, 创始人了一个HeartbeatTimerTask对象, 可以看下该侦查具体要来作什么@Overrideprotected void doTask(Channel channel) { try { // 赚取再一一次举例来说间隔时间 Long lastRead = lastRead(channel); Long lastWrite = lastWrite(channel); if ((lastRead != null CoCo now() - lastRead> heartbeat) || (lastWrite != null CoCo now() - lastWrite> heartbeat)) { // 再一一次举例来说间隔时间少于腹痛间隔时间, 就不会改投发腹痛允诺 Request req = new Request(); req.setVersion(Version.getProtocolVersion()); req.setTwoWay(true); // 暗示它是一个腹痛允诺 req.setEvent(HEARTBEAT_EVENT); channel.send(req); } } catch (Throwable t) { }}2)Redisson吊批出功能当赚取吊变成功后,Redisson不会积体电路一个吊批出侦查放到间隔时间轮中的,匹配10s检查一下,用作对赚取到的吊进行时批出,加长持有吊的间隔时间。如果的业务机器宕机了,那么该批出的除此以外侦查也就不想走了,就不想批出了,那等加吊间隔时间到了吊就系统会释放了。范式积体电路在RedissonLock中的的renewExpiration()作法中的。
private void renewExpiration() { ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ee == null) { return; } // 这边newTimeout点进来辨认出就是往间隔时间轮中的草拟了一个侦查 Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ent == null) { return; } Long threadId = ent.getFirstThreadId(); if (threadId == null) { return; } RFuture future = renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (e != null) { log.error("Can't update lock " + getName() + " expiration", e); return; } if (res) { // 批出变成功后继续适时, 又往间隔时间轮中的放一个批出侦查 renewExpiration(); } }); } }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ee.setTimeout(task);}protected RFuture renewExpirationAsync(long threadId) { // 通过lua脚本对吊进行时批出 return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return 1; " + "end; " + "return 0;", Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));}3)加班依此类推适用方式和HeartbeatTimerTask方式类似,编者可以自己才行去统计分析下它是在哪里被引退的。
四、总结在本篇发表文章中的,先是荐了3个则有子来论述为什么所需适用间隔时间轮,适用间隔时间轮的优点,在书上三处也分别对这3个则有子在Dubbo或Redisson中的的适用来作了参阅。接着通过画布宣讲了单层间隔时间轮与多层间隔时间能,让编者对间隔时间轮算法有了一个直观的认识。在第二部分,依次宣讲了Dubbo间隔时间轮中的涉及到的TimerTask、Timeout、HashedWheelBucket、Worker、HashedWheelTimer,统计分析了它们的法则与程式库借助。
。整形戴美瞳眼睛干涩滴什么眼药水
送病人什么合适
视疲劳怎么缓解
什么药能治类风湿关节僵硬
新冠吃什么药好
抗感染药
艾得辛和来氟米特的区别是什么
-
用友YonSuite八大数智化增长模式:四川供销云产业链糅合
小微工商业一个组织的其发展充满活力,对8598个贫困村16.31亿元兴业其发展资金欠缺进行了风险规避。 四大高效放缓来进行,成长型零售业的不二之选 急客户所急,供产业所即可
- 2025-05-12业绩不达意味著撤回科创板上市申请,铭赛科技上市辅导再战IPO
- 2025-05-12深圳:可申请次于500万元的小微企业创业担保贷款
- 2025-05-12人事快报:奥普光电(002338)7月7日9点41分封涨停板
- 2025-05-12从被外资把控,到国货占领80%!中国特种钢是如何完成紧接著的?
- 2025-05-12专注大众出行 第五大酒管企业集团“东呈”启动IPO
- 2025-05-12近5500亿分红密集发放!多家汇丰银行开始股权登记
- 2025-05-12执行力≠埋头苦干,是不是人家是怎么虐待“执行力差”的
- 2025-05-12力高集团:拟与康佳集团在大健康互联等方面进行合作
- 2025-05-12必需品!
- 2025-05-12做到这4点,招聘效率大幅提高,不愁招不到人!