Quartz应用与集群原理分析

一、问题背景

美团CRM系统中每天有大量的后台任务需要调度执行,如构建索引、统计报表、周期同步数据等等,要求任务调度系统具备高可用性、负载均衡特性,可以管理并监控任务的执行流程,以保证任务的正确执行。

二、历史方案

美团CRM系统的任务调度模块经历了以下历史方案。

1. Crontab+SQL

每天晚上运行定时任务,通过SQL脚本+crontab方式执行,例如,

#crm
0 2 * * * /xxx/mtcrm/shell/mtcrm_daily_stat.sql    //每天凌晨2:00执行统计
30 7 * * * /xxx/mtcrm/shell/mtcrm_data_fix.sql     //每天早上7:30执行数据修复

该方案存在以下问题:

  • 直接访问数据库,各系统业务接口没有重用。
  • 完成复杂业务需求时,会引入过多中间表。
  • 业务逻辑计算完全依赖SQL,增大数据库压力。
  • 任务失败无法自动恢复。

2. Python+SQL

采用python脚本(多数据源)+SQL方式执行,例如,

 def connectCRM():return MySQLdb.Connection("host1", "uname", "xxx", "crm", 3306, charset="utf8")def connectTemp():return MySQLdb.Connection("host1", "uname", "xxx", "temp", 3306, charset="utf8")

该方案存在问题:

  • 直接访问数据,需要理解各系统的数据结构,无法满足动态任务问题,各系统业务接口没有重用。
  • 无负载均衡。
  • 任务失败无法恢复。
  • 在JAVA语言开发中出现异构,且很难统一到自动部署系统中。

3. Spring+JDK Timer

该方案使用spring+JDK Timer方式,调用接口完成定时任务,在分布式部署环境下,防止多个节点同时运行任务,需要写死host,控制在一台服务器上执行task。

<bean id="accountStatusTaskScanner"  class="xxx.crm.service.impl.AccountStatusTaskScanner" /><task:scheduler id="taskScheduler" pool-size="5" /><task:scheduled-tasks scheduler="taskScheduler"><task:scheduled ref="accountStatusTaskScanner" method="execute" cron="0 0 1 * * ?" />
</task:scheduled-tasks>

该方案较方案1,2有很大改进,但仍存在以下问题:

  • 步骤复杂、分散,任务量增大的情况下,很难扩展
  • 使用写死服务器Host的方式执行task,存在单点风险,负载均衡手动完成。
  • 应用重启,任务无法自动恢复。

CRM系统定时任务走过了很多弯路:定时任务多种实现方式,使配置和代码分散在多处,难以维护和监控;任务执行过程没有保证,没有错误恢复;任务执行异常没有反馈(邮件);没有集群支持、负载均衡。CRM系统需要分布式的任务调度框架,统一解决问题,Java可以使用的任务调度框架有Quartz,Jcrontab,cron4j,我们选择了Quartz。

三、为什么选择Quartz

Quartz是Java领域最著名的开源任务调度工具。Quartz提供了极为广泛的特性如持久化任务,集群和分布式任务等,其特点如下:

  • 完全由Java写成,方便集成(Spring)
  • 伸缩性
  • 负载均衡
  • 高可用性

四、Quartz集群部署实践

CRM中Quartz与Spring结合使用,Spring通过提供org.springframework.scheduling.quartz下的封装类对Quartz支持。

Quartz集群部署:

Quartz集群部署

Quartz集群中的每个节点是一个独立的Quartz应用,它又管理着其他的节点。该集群需要分别对每个节点分别启动或停止,不像应用服务器的集群,独立的Quartz节点并不与另一个节点或是管理节点通信。Quartz应用是通过数据库表来感知到另一应用。只有使用持久的JobStore才能完成Quqrtz集群。

基于Spring的集群配置:

<!-- 调度工厂 -->
<bean id="quartzScheduler"class="org.springframework.scheduling.quartz.SchedulerFactoryBean"><property name="dataSource" ref="dataSource" /><property name="quartzProperties"><props><prop key="org.quartz.scheduler.instanceName">CRMscheduler</prop><prop key="org.quartz.scheduler.instanceId">AUTO</prop><!-- 线程池配置 --><prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop><prop key="org.quartz.threadPool.threadCount">20</prop><prop key="org.quartz.threadPool.threadPriority">5</prop><!-- JobStore 配置 --><prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop><!-- 集群配置 --><prop key="org.quartz.jobStore.isClustered">true</prop><prop key="org.quartz.jobStore.clusterCheckinInterval">15000</prop><prop key="org.quartz.jobStore.maxMisfiresToHandleAtATime">1</prop><prop key="org.quartz.jobStore.misfireThreshold">120000</prop><prop key="org.quartz.jobStore.tablePrefix">QRTZ_</prop></props></property><property name="schedulerName" value="CRMscheduler" /><!--必须的,QuartzScheduler 延时启动,应用启动完后 QuartzScheduler 再启动 --><property name="startupDelay" value="30" /><property name="applicationContextSchedulerContextKey" value="applicationContextKey" /><!--可选,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 --><property name="overwriteExistingJobs" value="true" /><!-- 设置自动启动 --><property name="autoStartup" value="true" /><!-- 注册触发器 --><property name="triggers"><list><ref bean="userSyncScannerTrigger" />......</list></property><!-- 注册jobDetail --><property name="jobDetails"><list></list></property><property name="schedulerListeners"><list><ref bean="quartzExceptionSchedulerListener" /></list></property>
</bean>org.quartz.jobStore.class属性为JobStoreTX,将任务持久化到数据中。因为集群中节点依赖于数据库来传播Scheduler实例的状态,你只能在使用JDBC JobStore时应用Quartz集群。org.quartz.jobStore.isClustered属性为true,通知Scheduler实例要它参与到一个集群当中。org.quartz.jobStore.clusterCheckinInterval属性定义了Scheduler实例检入到数据库中的频率(单位:毫秒)。Scheduler检查是否其他的实例到了它们应当检入的时候未检入;这能指出一个失败的Scheduler实例,且当前 Scheduler会以此来接管任何执行失败并可恢复的Job。通过检入操作,Scheduler 也会更新自身的状态记录。clusterChedkinInterval越小,Scheduler节点检查失败的Scheduler实例就越频繁。默认值是 15000 (即15 秒)。其余参数在后文将会详细介绍。

Quartz监控

CRM后台目前可以做到对Quartz实例的监控、操作以及动态部署Trigger.

Triggers监控:

Quartz Triggers

JobDetails监控:

Quartz Jobs

五、Quartz集群原理分析

1. Quartz集群数据库表

Quartz的集群部署方案在架构上是分布式的,没有负责集中管理的节点,而是利用数据库锁的方式来实现集群环境下进行并发控制。BTW,分布式部署时需要保证各个节点的系统时间一致。

Quartz数据库核心表如下:

Table NameDescription
QRTZ_CALENDARS存储Quartz的Calendar信息
QRTZ_CRON_TRIGGERS存储CronTrigger,包括Cron表达式和时区信息
QRTZ_FIRED_TRIGGERS存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息
QRTZ_PAUSED_TRIGGER_GRPS存储已暂停的Trigger组的信息
QRTZ_SCHEDULER_STATE存储少量的有关Scheduler的状态信息,和别的Scheduler实例
QRTZ_LOCKS存储程序的悲观锁的信息
QRTZ_JOB_DETAILS存储每一个已配置的Job的详细信息
QRTZ_JOB_LISTENERS存储有关已配置的JobListener的信息
QRTZ_SIMPLE_TRIGGERS存储简单的Trigger,包括重复次数、间隔、以及已触的次数
QRTZ_BLOG_TRIGGERSTrigger作为Blob类型存储
QRTZ_TRIGGER_LISTENERS存储已配置的TriggerListener的信息
QRTZ_TRIGGERS存储已配置的Trigger的信息

其中,QRTZ_LOCKS就是Quartz集群实现同步机制的行锁表,其表结构如下:

--QRTZ_LOCKS表结构
CREATE TABLE `QRTZ_LOCKS` (`LOCK_NAME` varchar(40) NOT NULL,PRIMARY KEY (`LOCK_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;--QRTZ_LOCKS记录
+-----------------+ 
| LOCK_NAME       |
+-----------------+ 
| CALENDAR_ACCESS |
| JOB_ACCESS      |
| MISFIRE_ACCESS  |
| STATE_ACCESS    |
| TRIGGER_ACCESS  |
+-----------------+

可以看出QRTZ_LOCKS中有5条记录,代表5把锁,分别用于实现多个Quartz Node对Job、Trigger、Calendar访问的同步控制。

2. Quartz线程模型

在Quartz中有两类线程:Scheduler调度线程和任务执行线程。*任务执行线程*:Quartz不会在主线程(QuartzSchedulerThread)中处理用户的Job。Quartz把线程管理的职责委托给ThreadPool,一般的设置使用SimpleThreadPool。SimpleThreadPool创建了一定数量的WorkerThread实例来使得Job能够在线程中进行处理。WorkerThread是定义在SimpleThreadPool类中的内部类,它实质上就是一个线程。例如,CRM中配置如下:

 <!-- 线程池配置 --><prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop><prop key="org.quartz.threadPool.threadCount">20</prop><prop key="org.quartz.threadPool.threadPriority">5</prop>

*QuartzSchedulerThread调度主线程*:QuartzScheduler被创建时创建一个QuartzSchedulerThread实例。

3. 集群源码分析

Quartz究竟是如何保证集群情况下trgger处理的信息同步?

下面跟着源码一步一步分析,QuartzSchedulerThread包含有决定何时下一个Job将被触发的处理循环,主要逻辑在其run()方法中:

public void run() {boolean lastAcquireFailed = false;while (!halted.get()) {......int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();if(availThreadCount > 0) { ......//调度器在trigger队列中寻找30秒内一定数目的trigger(需要保证集群节点的系统时间一致)triggers = qsRsrcs.getJobStore().acquireNextTriggers(now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());......//触发triggerList<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);......//释放triggerfor (int i = 0; i < triggers.size(); i++) {qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));}}                
}

由此可知,QuartzScheduler调度线程不断获取trigger,触发trigger,释放trigger。下面分析trigger的获取过程,qsRsrcs.getJobStore()返回对象是JobStore,集群环境配置如下:

<!-- JobStore 配置 -->
<prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop>

JobStoreTX继承自JobStoreSupport,而JobStoreSupport的acquireNextTriggers、triggersFired、releaseAcquiredTrigger方法负责具体trigger相关操作,都必须获得TRIGGER_ACCESS锁。核心逻辑在executeInNonManagedTXLock方法中:

protected <T> T executeInNonManagedTXLock(String lockName, TransactionCallback<T> txCallback, final TransactionValidator<T> txValidator) throws JobPersistenceException {boolean transOwner = false;Connection conn = null;try {if (lockName != null) {if (getLockHandler().requiresConnection()) {conn = getNonManagedTXConnection();}//获取锁transOwner = getLockHandler().obtainLock(conn, lockName);}if (conn == null) {conn = getNonManagedTXConnection();}final T result = txCallback.execute(conn);try {commitConnection(conn);} catch (JobPersistenceException e) {rollbackConnection(conn);if (txValidator == null || !retryExecuteInNonManagedTXLock(lockName, new TransactionCallback<Boolean>() {@Overridepublic Boolean execute(Connection conn) throws JobPersistenceException {return txValidator.validate(conn, result);}})) {throw e;}}Long sigTime = clearAndGetSignalSchedulingChangeOnTxCompletion();if(sigTime != null && sigTime >= 0) {signalSchedulingChangeImmediately(sigTime);}return result;} catch (JobPersistenceException e) {rollbackConnection(conn);throw e;} catch (RuntimeException e) {rollbackConnection(conn);throw new JobPersistenceException("Unexpected runtime exception: "+ e.getMessage(), e);} finally {try {releaseLock(lockName, transOwner);      //释放锁} finally {cleanupConnection(conn);}}
}

由上代码可知Quartz集群基于数据库锁的同步操作流程如下图所示:

Quartz集群基于锁的同步方案

一个调度器实例在执行涉及到分布式问题的数据库操作前,首先要获取QUARTZ_LOCKS表中对应的行级锁,获取锁后即可执行其他表中的数据库操作,随着操作事务的提交,行级锁被释放,供其他调度实例获取。集群中的每一个调度器实例都遵循这样一种严格的操作规程。

getLockHandler()方法返回的对象类型是Semaphore,获取锁和释放锁的具体逻辑由该对象维护

public interface Semaphore {boolean obtainLock(Connection conn, String lockName) throws LockException;void releaseLock(String lockName) throws LockException;boolean requiresConnection();
}

该接口的实现类完成具体操作锁的逻辑,在JobStoreSupport的初始化方法中注入的Semaphore具体类型是StdRowLockSemaphore

setLockHandler(new StdRowLockSemaphore(getTablePrefix(), getInstanceName(), getSelectWithLockSQL()));

StdRowLockSemaphore的源码如下所示:

public class StdRowLockSemaphore extends DBSemaphore {
//锁定SQL语句
public static final String SELECT_FOR_LOCK = "SELECT * FROM "+ TABLE_PREFIX_SUBST + TABLE_LOCKS + " WHERE " + COL_LOCK_NAME+ " = ? FOR UPDATE";public static final String INSERT_LOCK = "INSERT INTO " + TABLE_PREFIX_SUBST + TABLE_LOCKS + "(" + COL_SCHEDULER_NAME + ", " + COL_LOCK_NAME + ") VALUES (" + SCHED_NAME_SUBST + ", ?)"; //指定锁定SQL
protected void executeSQL(Connection conn, String lockName, String expandedSQL) throws LockException {PreparedStatement ps = null;ResultSet rs = null;try {ps = conn.prepareStatement(expandedSQL);ps.setString(1, lockName);......rs = ps.executeQuery();if (!rs.next()) {throw new SQLException(Util.rtp("No row exists in table " + TABLE_PREFIX_SUBST +TABLE_LOCKS + " for lock named: " + lockName, getTablePrefix()));}} catch (SQLException sqle) {} finally {...... //release resources}}
}//获取QRTZ_LOCKS行级锁
public boolean obtainLock(Connection conn, String lockName) throws LockException {lockName = lockName.intern();if (!isLockOwner(conn, lockName)) {executeSQL(conn, lockName, expandedSQL);getThreadLocks().add(lockName);}return true;
}//释放QRTZ_LOCKS行级锁
public void releaseLock(Connection conn, String lockName) {lockName = lockName.intern();if (isLockOwner(conn, lockName)) {getThreadLocks().remove(lockName);}......
}

至此,总结一下Quartz集群同步机制:每当要进行与某种业务相关的数据库操作时,先去QRTZ_LOCKS表中查询操作相关的业务对象所需要的锁,在select语句之后加for update来实现。例如,TRIGGER_ACCESS表示对任务触发器相关的信息进行修改、删除操作时所需要获得的锁。这时,执行查询这个表数据的SQL形如:

select * from QRTZ_LOCKS t where t.lock_name='TRIGGER_ACCESS' for update

当一个线程使用上述的SQL对表中的数据执行查询操作时,若查询结果中包含相关的行,数据库就对该行进行ROW LOCK;若此时,另外一个线程使用相同的SQL对表的数据进行查询,由于查询出的数据行已经被数据库锁住了,此时这个线程就只能等待,直到拥有该行锁的线程完成了相关的业务操作,执行了commit动作后,数据库才会释放了相关行的锁,这个线程才能继续执行。

通过这样的机制,在集群环境下,结合悲观锁的机制就可以防止一个线程对数据库数据的操作的结果被另外一个线程所覆盖,从而可以避免一些难以觉察的错误发生。当然,达到这种效果的前提是需要把Connection设置为手动提交,即autoCommit为false。

六、参考资料

  • http://quartz-scheduler.org/documentation Quartz Documentation
  • http://www.ibm.com/developerworks/cn/opensource/os-cn-quartz 基于Quartz开发企业级任务调度应用

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

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

相关文章

卖萌屋新闻联播栏目,倾情上线~

编 | 小轶感谢提供本期内容的 iven、ZenMoore、 jxyxiangyu、付瑶今天这篇推文是卖萌屋全新的原创系列———暂且取名为“卖萌屋新闻联播”节目。卖萌屋的作者、小编日常都会在团队群里分享各种最新发现的实用资源、有意思的学术工作。小伙伴们在互相分享的过程中都受益匪浅。我…

LeetCode 386. 字典序排数(DFS循环)

1. 题目 给定一个整数 n, 返回从 1 到 n 的字典顺序。 例如&#xff0c; 给定 n 1 3&#xff0c;返回 [1,10,11,12,13,2,3,4,5,6,7,8,9] 。 请尽可能的优化算法的时间复杂度和空间复杂度。 输入的数据 n 小于等于 5,000,000。来源&#xff1a;力扣&#xff08;LeetCode&#…

论文浅尝 | 基于多模态特征的视觉实体链接

转载公众号 | 数据智能英文刊文章题目&#xff1a;Visual Entity Linking via Multi-modal Learning作者&#xff1a;郑秋硕&#xff0c;闻浩&#xff0c;王萌&#xff0c;漆桂林引用&#xff1a;Zheng, Q.S., et al.: Visual Entity Linking via Multi-modal Learning. Data I…

一训练就显存爆炸?Facebook 推出 8 比特优化器,两行代码拯救你的显存!

文 | jxyxiangyu编 | 小轶“小夕&#xff0c;小夕&#xff01;又出来了个 SOTA 模型&#xff01;赶紧 follow &#xff01;”小夕看了看新模型的参数量&#xff0c; 然后看了看实验室服务器的几张小破卡。小夕&#xff0c;陷入了沉默。自从人们发现越大的模型性能越好后&#x…

论文浅尝 | 基于正交普鲁克分析的高效知识图嵌入学习

笔记整理&#xff1a;朱渝珊&#xff0c;浙江大学在读博士&#xff0c;研究方向为快速知识图谱的表示学习&#xff0c;多模态知识图谱。1.Motivation知识图谱是许多NLP任务和下游应用的核心&#xff0c;如问答、对话代理、搜索引擎和推荐系统。知识图中存储的事实总是以元组的形…

LeetCode 979. 在二叉树中分配硬币(DFS)

文章目录1. 题目2. DFS 解题1. 题目 给定一个有 N 个结点的二叉树的根结点 root&#xff0c;树中的每个结点上都对应有 node.val 枚硬币&#xff0c;并且总共有 N 枚硬币。 在一次移动中&#xff0c;我们可以选择两个相邻的结点&#xff0c;然后将一枚硬币从其中一个结点移动…

有福利! 好书推荐:从《实用推荐系统》学习寻找用户行为之法

大多数关于推荐系统的图书都讲述了算法及其优化方法。这些书都认为你已经有了一个大的数据集来供算法使用。数据集不会像变魔术那样凭空出现。要想收集到正确的用户偏好数据&#xff0c;就需要投入精力和进行思考。它会成就你的系统&#xff0c;或者搞砸你的系统。“垃圾进&…

灵活强大的构建系统Gradle

前言 构建&#xff0c;软件生命周期中重要的一环&#xff0c;在现代软件开发过程中&#xff0c;起着越来越重要的作用。过去在Java或类Java的世界里&#xff0c;Ant、Maven再熟悉不过了&#xff0c;Maven凭借其强大的依赖配置战胜Ant&#xff0c;基本上成为了Java构建的标准。而…

LeetCode 791. 自定义字符串排序(map)

1. 题目 字符串S和 T 只包含小写字符。在S中&#xff0c;所有字符只会出现一次。 S 已经根据某种规则进行了排序。我们要根据S中的字符顺序对T进行排序。更具体地说&#xff0c;如果S中x在y之前出现&#xff0c;那么返回的字符串中x也应出现在y之前。 返回任意一种符合条件的…

6万字解决算法面试中的深度学习基础问题

文 | 清卢雨源 | 对白的算法屋前言真的是千呼万唤始出来emmmm&#xff0c;去年春招结束写了篇面试的经验分享。在文中提到和小伙伴整理了算法岗面试时遇到的常见知识点及回答&#xff0c;本想着授人以渔&#xff0c;但没想到大家都看上了我家的 &#xff01;但因本人执行力不足…

OpenKG开源系列 | 海洋鱼类百科知识图谱(浙江大学)

OpenKG地址&#xff1a;http://openkg.cn/dataset/ocean开放许可协议&#xff1a;CC BY-SA 4.0贡献者&#xff1a;浙江大学&#xff08;徐雅静、邓鸿杰、唐坤、郑国轴&#xff09;1、背景海洋是生命的摇篮,是人类文明的重要发祥地,在人类社会发展的进程中起着举足轻重的作用。海…

Presto实现原理和美团的使用实践

Facebook的数据仓库存储在少量大型Hadoop/HDFS集群。Hive是Facebook在几年前专为Hadoop打造的一款数据仓库工具。在以前&#xff0c;Facebook的科学家和分析师一直依靠Hive来做数据分析。但Hive使用MapReduce作为底层计算框架&#xff0c;是专为批处理设计的。但随着数据越来越…

图谱实战 | 徐美兰:深度应用驱动的医学知识图谱构建

转载公众号 | DataFunSummit分享嘉宾&#xff1a;徐美兰 浙江数字医疗卫生技术研究院 数字医学知识中心主任编辑整理&#xff1a;李杰 京东出品平台&#xff1a;DataFunTalk导读&#xff1a;数研院这些年在知识图谱建设上取得了丰硕成果&#xff0c;今天我们将图谱构建过程中的…

6 年大厂面试官,谈谈我对算法岗面试的一些看法

文 | 不敢透露姓名的 Severus 和小轶面试官坐在那撇着大嘴的&#xff0c;“咳&#xff0c;给你一机会&#xff0c;最短的时间内让我记住你。”这个我会&#xff0c;我抡圆了“啪&#xff01;”&#xff0c;扭头我就走。我刚到家&#xff0c;录取通知书就来了&#xff0c;请你务…

美团Android自动化之旅—生成渠道包

每当发新版本时&#xff0c;美团团购Android客户端会被分发到各个应用市场&#xff0c;比如豌豆荚&#xff0c;360手机助手等。为了统计这些市场的效果&#xff08;活跃数&#xff0c;下单数等&#xff09;&#xff0c;需要有一种方法来唯一标识它们。 团购客户端目前通过渠道号…

开源开放 | 细粒度可循证医学文档知识融合表示和推理(CCKS2021)

OpenKG地址&#xff1a;http://openkg.cn/dataset/mdo-dataset开放许可协议&#xff1a;GPL 3.0贡献者&#xff1a;武汉科技大学&#xff08;高峰、龚珊珊、顾进广、徐芳芳&#xff09;摘要本开放资源在医学文档知识的基础上&#xff0c;使用知识图谱相关技术&#xff0c;解决了…

图灵奖大佬 Lecun 发表对比学习新作,比 SimCLR 更好用!

文 | Rukawa_Y编 | 智商掉了一地&#xff0c;Sheryc_王苏比 SimCLR 更好用的 Self-Supervised Learning&#xff0c;一起来看看吧&#xff01;Self-Supervised Learning作为深度学习中的独孤九剑&#xff0c;当融汇贯通灵活应用之后&#xff0c;也能打败声名在外的武当太极剑。…

5whys分析法在美团工程师中的实践

前言 网站的质量和稳定性对于用户和公司来说至关重要&#xff0c;但是在网站的快速发展过程中&#xff0c;由于各种原因导致事故不可避免的发生&#xff0c;这些大大小小的事故对公司难免会造成一些负面的影响&#xff0c;为了避免同类事故的再次发生&#xff0c;美团的工程师们…

LeetCode 382. 链表随机节点(概率)

1. 题目 给定一个单链表&#xff0c;随机选择链表的一个节点&#xff0c;并返回相应的节点值。保证每个节点被选的概率一样。 进阶: 如果链表十分大且长度未知&#xff0c;如何解决这个问题&#xff1f;你能否使用常数级空间复杂度实现&#xff1f; 来源&#xff1a;力扣&am…

图谱实战 | 斯坦福黄柯鑫:图机器学习在生物图上的应用

转载公众号 | DataFunSummit分享嘉宾&#xff1a;黄柯鑫 斯坦福大学 博士生编辑整理&#xff1a;元玉蒲 西北大学出品平台&#xff1a;DataFunTalk导读&#xff1a;大家好&#xff0c;我叫黄柯鑫。我现在是斯坦福大学的计算机科学博士第一年级&#xff0c;研究方向是机器学习在…