我们知道,如果想要在Yarn上运行MapReduce作业,仅需实现一个ApplicationMaster组件即可,而MRAppMaster正是MapReduce在Yarn上ApplicationMaster的实现,由其控制MR作业在Yarn上的执行。如此,随之而来的一个问题就是,MRAppMaster是如何控制MapReduce作业在Yarn上运行的,换句话说,MRAppMaster上MapReduce作业处理总流程是什么?这就是本文要研究的重点。

通过MRAppMaster类的定义我们就能看出,MRAppMaster继承自CompositeService,而CompositeService又继承自AbstractService,也就是说MRAppMaster也是Hadoop中的一种服务,我们看下服务启动的serviceStart()方法中关于MapReduce作业的处理,关键代码如下:

  1. @SuppressWarnings("unchecked")
  2. @Override
  3. protected void serviceStart() throws Exception {
  4. // ......省略部分代码
  5. // 调用createJob()方法创建作业Job实例job
  6. // /////////////////// Create the job itself.
  7. job = createJob(getConfig(), forcedState, shutDownMessage);
  8. // End of creating the job.
  9. // ......省略部分代码
  10. // 作业初始化失败标志位initFailed默认为false,即初始化成功,没有错误
  11. boolean initFailed = false;
  12. if (!errorHappenedShutDown) {
  13. // create a job event for job intialization
  14. // 创建一个Job初始化事件initJobEvent
  15. JobEvent initJobEvent = new JobEvent(job.getID(), JobEventType.JOB_INIT);
  16. // Send init to the job (this does NOT trigger job execution)
  17. // This is a synchronous call, not an event through dispatcher. We want
  18. // job-init to be done completely here.
  19. // 调用jobEventDispatcher的handle()方法,处理Job初始化事件initJobEvent,即将Job初始化事件交由事件分发器jobEventDispatcher处理,
  20. jobEventDispatcher.handle(initJobEvent);
  21. // If job is still not initialized, an error happened during
  22. // initialization. Must complete starting all of the services so failure
  23. // events can be processed.
  24. // 获取Job初始化结果initFailed
  25. initFailed = (((JobImpl)job).getInternalState() != JobStateInternal.INITED);
  26. // JobImpl's InitTransition is done (call above is synchronous), so the
  27. // "uber-decision" (MR-1220) has been made.  Query job and switch to
  28. // ubermode if appropriate (by registering different container-allocator
  29. // and container-launcher services/event-handlers).
  30. // ......省略部分代码
  31. // Start ClientService here, since it's not initialized if
  32. // errorHappenedShutDown is true
  33. // 启动客户端服务clientService
  34. clientService.start();
  35. }
  36. //start all the components
  37. // 调用父类的serviceStart(),启动所有组件
  38. super.serviceStart();
  39. // finally set the job classloader
  40. // 最终设置作业类加载器
  41. MRApps.setClassLoader(jobClassLoader, getConfig());
  42. if (initFailed) {
  43. // 如果作业初始化失败,构造作业初始化失败JOB_INIT_FAILED事件,并交由事件分发器jobEventDispatcher处理
  44. JobEvent initFailedEvent = new JobEvent(job.getID(), JobEventType.JOB_INIT_FAILED);
  45. jobEventDispatcher.handle(initFailedEvent);
  46. } else {
  47. // All components have started, start the job.
  48. // 调用startJobs()方法启动作业
  49. startJobs();
  50. }
  51. }

通过MRAppMaster服务启动的serviceStart()方法我们大致知道,MapReduce作业在MRAppMaster中经历了创建--初始化--启动三个主要过程,剪去枝叶,保留主干,具体如下:

1、创建:调用createJob()方法创建作业Job实例job;

2、初始化:

2.1、创建一个Job初始化事件initJobEvent;

2.2、调用jobEventDispatcher的handle()方法,处理Job初始化事件initJobEvent,即将Job初始化事件交由事件分发器jobEventDispatcher处理;

2.3、获取Job初始化结果initFailed;

2.4、如果作业初始化失败,构造作业初始化失败JOB_INIT_FAILED事件,并交由事件分发器jobEventDispatcher处理。

3、启动:调用startJobs()方法启动作业。

实际上,作业启动后不可能永远都不停止,MRAppMaster最终会将作业停止,这也是作业处理流程的第四步,即最后一步,作业停止!在哪里处理的呢?我们先卖个关子,请您暂时忽略这个问题,我们稍后会给出答案!

下面,我们针对MapReduce作业的上述三个主要过程,分别展开描述。

一、创建

首先看作业创建,createJob()方法如下:

  1. /** Create and initialize (but don't start) a single job.
  2. * @param forcedState a state to force the job into or null for normal operation.
  3. * @param diagnostic a diagnostic message to include with the job.
  4. */
  5. protected Job createJob(Configuration conf, JobStateInternal forcedState,
  6. String diagnostic) {
  7. // create single job
  8. / 创建一个作业Job实例newJob,其实现为JobImpl
  9. Job newJob =
  10. new JobImpl(jobId, appAttemptID, conf, dispatcher.getEventHandler(),
  11. taskAttemptListener, jobTokenSecretManager, jobCredentials, clock,
  12. completedTasksFromPreviousRun, metrics,
  13. committer, newApiCommitter,
  14. currentUser.getUserName(), appSubmitTime, amInfos, context,
  15. forcedState, diagnostic);
  16. // 将新创建的作业newJob的jobId与其自身的映射关系存储到应用运行上下文信息context中的jobs集合中
  17. ((RunningAppContext) context).jobs.put(newJob.getID(), newJob);
  18. // 异步事件分发器dispatcher注册作业完成事件JobFinishEvent对应的事件处理器,通过createJobFinishEventHandler()方法获得
  19. dispatcher.register(JobFinishEvent.Type.class,
  20. createJobFinishEventHandler());
  21. // 返回新创建的作业newJob
  22. return newJob;
  23. } // end createJob()

其主要逻辑如下:

1、创建一个作业Job实例newJob,其实现为JobImpl,传入作业艾迪jobId、应用尝试艾迪appAttemptID、任务尝试监听器taskAttemptListener、输出提交器committer、用户名currentUser.getUserName()、应用运行上下文信息context等关键成员变量;

2、将新创建的作业newJob的jobId与其自身的映射关系存储到应用运行上下文信息context中的jobs集合中;

3、异步事件分发器dispatcher注册作业完成事件JobFinishEvent对应的事件处理器,通过createJobFinishEventHandler()方法获得;

4、返回新创建的作业newJob。

关于作业创建中的一些细节,我们暂时先不做过多关注,留待以后的文章专门进行分析。这里,我们先重点看看第3步,异步事件分发器dispatcher注册作业完成事件JobFinishEvent对应的事件处理器,通过createJobFinishEventHandler()方法获得,而createJobFinishEventHandler()方法代码如下:

  1. /**
  2. * create an event handler that handles the job finish event.
  3. * @return the job finish event handler.
  4. */
  5. protected EventHandler<JobFinishEvent> createJobFinishEventHandler() {
  6. return new JobFinishEventHandler();
  7. }

也就是说,当作业被创建后,它就被定义了作业完成事件JobFinishEvent的处理器为JobFinishEventHandler,而JobFinishEventHandler的定义如下:

  1. private class JobFinishEventHandler implements EventHandler<JobFinishEvent> {
  2. @Override
  3. public void handle(JobFinishEvent event) {
  4. // Create a new thread to shutdown the AM. We should not do it in-line
  5. // to avoid blocking the dispatcher itself.
  6. new Thread() {
  7. @Override
  8. public void run() {
  9. shutDownJob();
  10. }
  11. }.start();
  12. }
  13. }

这就是我们上面没有详细介绍的第四步--作业停止,它最终是调用的shutDownJob()方法,并开启一个新的线程来完成作业停止的,我们稍后再做介绍。

二、初始化

我们再来看作业的初始化,它是通过创建一个Job初始化事件JobEvent实例initJobEvent,事件类型为JobEventType.JOB_INIT,然后交由事件分发器jobEventDispatcher处理的。我们先来看下这个jobEventDispatcher的定义及实例化,如下:

  1. // 作业事件分发器
  2. private JobEventDispatcher jobEventDispatcher;

jobEventDispatcher是一个JobEventDispatcher类型的作业事件分发器,其实例化为:

  1. this.jobEventDispatcher = new JobEventDispatcher();

而JobEventDispatcher的定义如下:

  1. private class JobEventDispatcher implements EventHandler<JobEvent> {
  2. @SuppressWarnings("unchecked")
  3. @Override
  4. public void handle(JobEvent event) {
  5. // 从应用运行上下文信息context中根据jobId获取Job实例,即JobImpl对象,调用其handle()方法,处理对应事件
  6. ((EventHandler<JobEvent>)context.getJob(event.getJobId())).handle(event);
  7. }
  8. }

很简单,从应用运行上下文信息context中根据jobId获取Job实例,即JobImpl对象,调用其handle()方法,处理对应事件,而这个Job实例,还记得上面描述的吗,就是在Job最初被创建时,被添加到应用运行上下文信息context中jobs集合中的,key为jobId,value就是JobImpl对象。context的实现RunningAppContext中,根据jobId获取job实例的代码如下:

  1. @Override
  2. public Job getJob(JobId jobID) {
  3. return jobs.get(jobID);
  4. }

好了,我们就看下JobImpl中handle()方法是如何对类型为JobEventType.JOB_INIT的JobEvent进行处理的吧!

  1. @Override
  2. /**
  3. * The only entry point to change the Job.
  4. */
  5. public void handle(JobEvent event) {
  6. if (LOG.isDebugEnabled()) {
  7. LOG.debug("Processing " + event.getJobId() + " of type "
  8. + event.getType());
  9. }
  10. try {
  11. writeLock.lock();
  12. JobStateInternal oldState = getInternalState();
  13. try {
  14. getStateMachine().doTransition(event.getType(), event);
  15. } catch (InvalidStateTransitonException e) {
  16. LOG.error("Can't handle this event at current state", e);
  17. addDiagnostic("Invalid event " + event.getType() +
  18. " on Job " + this.jobId);
  19. eventHandler.handle(new JobEvent(this.jobId,
  20. JobEventType.INTERNAL_ERROR));
  21. }
  22. //notify the eventhandler of state change
  23. if (oldState != getInternalState()) {
  24. LOG.info(jobId + "Job Transitioned from " + oldState + " to "
  25. + getInternalState());
  26. rememberLastNonFinalState(oldState);
  27. }
  28. }
  29. finally {
  30. writeLock.unlock();
  31. }
  32. }

最核心的就是通过语句getStateMachine().doTransition(event.getType(), event)进行处理,实际上这牵着到了Yarn中MapReduce作业的状态机,为了本文叙述的流畅性、简洁性、重点明确性,我们对于作业状态机先不做解释,这部分内容留待以后的文章专门进行介绍,这里你只要知道作业初始化最终是通过JobImpl静态内部类InitTransition的transition()方法来实现的就行。我们看下InitTransition的transition()方法,如下:

  1. /**
  2. * Note that this transition method is called directly (and synchronously)
  3. * by MRAppMaster's init() method (i.e., no RPC, no thread-switching;
  4. * just plain sequential call within AM context), so we can trigger
  5. * modifications in AM state from here (at least, if AM is written that
  6. * way; MR version is).
  7. */
  8. @Override
  9. public JobStateInternal transition(JobImpl job, JobEvent event) {
  10. // 调用作业度量指标体系metrics的submittedJob()方法,提交作业
  11. job.metrics.submittedJob(job);
  12. // 调用作业度量指标体系metrics的preparingJob()方法,开始作业准备
  13. job.metrics.preparingJob(job);
  14. // 新旧API创建不同的作业上下文JobContextImpl实例
  15. if (job.newApiCommitter) {
  16. job.jobContext = new JobContextImpl(job.conf,
  17. job.oldJobId);
  18. } else {
  19. job.jobContext = new org.apache.hadoop.mapred.JobContextImpl(
  20. job.conf, job.oldJobId);
  21. }
  22. try {
  23. // 调用setup()方法,完成作业启动前的部分初始化工作
  24. setup(job);
  25. // 设置作业job对应的文件系统fs
  26. job.fs = job.getFileSystem(job.conf);
  27. //log to job history
  28. // 创建作业已提交事件JobSubmittedEvent实例jse
  29. JobSubmittedEvent jse = new JobSubmittedEvent(job.oldJobId,
  30. job.conf.get(MRJobConfig.JOB_NAME, "test"),
  31. job.conf.get(MRJobConfig.USER_NAME, "mapred"),
  32. job.appSubmitTime,
  33. job.remoteJobConfFile.toString(),
  34. job.jobACLs, job.queueName,
  35. job.conf.get(MRJobConfig.WORKFLOW_ID, ""),
  36. job.conf.get(MRJobConfig.WORKFLOW_NAME, ""),
  37. job.conf.get(MRJobConfig.WORKFLOW_NODE_NAME, ""),
  38. getWorkflowAdjacencies(job.conf),
  39. job.conf.get(MRJobConfig.WORKFLOW_TAGS, ""));
  40. // 将作业已提交事件JobSubmittedEvent实例jse封装成作业历史事件JobHistoryEvent交由作业的时事件处理器eventHandler处理
  41. job.eventHandler.handle(new JobHistoryEvent(job.jobId, jse));
  42. //TODO JH Verify jobACLs, UserName via UGI?
  43. // 调用createSplits()方法,创建分片,并获取任务分片元数据信息TaskSplitMetaInfo数组taskSplitMetaInfo
  44. TaskSplitMetaInfo[] taskSplitMetaInfo = createSplits(job, job.jobId);
  45. // 确定Map Task数目numMapTasks:分片元数据信息数组的长度,即有多少分片就有多少numMapTasks
  46. job.numMapTasks = taskSplitMetaInfo.length;
  47. // 确定Reduce Task数目numReduceTasks,取作业参数mapreduce.job.reduces,参数未配置默认为0
  48. job.numReduceTasks = job.conf.getInt(MRJobConfig.NUM_REDUCES, 0);
  49. // 确定作业的map和reduce权重mapWeight、reduceWeight
  50. if (job.numMapTasks == 0 && job.numReduceTasks == 0) {
  51. job.addDiagnostic("No of maps and reduces are 0 " + job.jobId);
  52. } else if (job.numMapTasks == 0) {
  53. job.reduceWeight = 0.9f;
  54. } else if (job.numReduceTasks == 0) {
  55. job.mapWeight = 0.9f;
  56. } else {
  57. job.mapWeight = job.reduceWeight = 0.45f;
  58. }
  59. checkTaskLimits();
  60. // 根据分片元数据信息计算输入长度inputLength,也就是作业大小
  61. long inputLength = 0;
  62. for (int i = 0; i < job.numMapTasks; ++i) {
  63. inputLength += taskSplitMetaInfo[i].getInputDataLength();
  64. }
  65. // 根据作业大小inputLength,调用作业的makeUberDecision()方法,决定作业运行模式是Uber模式还是Non-Uber模式
  66. job.makeUberDecision(inputLength);
  67. // 根据作业的Map、Reduce任务数目之和,外加10,
  68. // 初始化任务尝试完成事件TaskAttemptCompletionEvent列表taskAttemptCompletionEvents
  69. job.taskAttemptCompletionEvents =
  70. new ArrayList<TaskAttemptCompletionEvent>(
  71. job.numMapTasks + job.numReduceTasks + 10);
  72. // 根据作业的Map任务数目,外加10,
  73. // 初始化Map任务尝试完成事件TaskCompletionEvent列表mapAttemptCompletionEvents
  74. job.mapAttemptCompletionEvents =
  75. new ArrayList<TaskCompletionEvent>(job.numMapTasks + 10);
  76. // 根据作业的Map、Reduce任务数目之和,外加10,
  77. // 初始化列表taskCompletionIdxToMapCompletionIdx
  78. job.taskCompletionIdxToMapCompletionIdx = new ArrayList<Integer>(
  79. job.numMapTasks + job.numReduceTasks + 10);
  80. // 确定允许Map、Reduce任务失败百分比,
  81. // 取参数mapreduce.map.failures.maxpercent、mapreduce.reduce.failures.maxpercent,
  82. // 参数未配置均默认为0,即不允许Map和Reduce任务失败
  83. job.allowedMapFailuresPercent =
  84. job.conf.getInt(MRJobConfig.MAP_FAILURES_MAX_PERCENT, 0);
  85. job.allowedReduceFailuresPercent =
  86. job.conf.getInt(MRJobConfig.REDUCE_FAILURES_MAXPERCENT, 0);
  87. // create the Tasks but don't start them yet
  88. // 创建Map Task
  89. createMapTasks(job, inputLength, taskSplitMetaInfo);
  90. // 创建Reduce Task
  91. createReduceTasks(job);
  92. // 调用作业度量指标体系metrics的endPreparingJob()方法,结束作业准备
  93. job.metrics.endPreparingJob(job);
  94. // 返回作业内部状态,JobStateInternal.INITED,即已经初始化
  95. return JobStateInternal.INITED;
  96. } catch (Exception e) {
  97. // 记录warn级别日志信息:Job init failed,并打印出具体异常
  98. LOG.warn("Job init failed", e);
  99. // 调用作业度量指标体系metrics的endPreparingJob()方法,结束作业准备
  100. job.metrics.endPreparingJob(job);
  101. job.addDiagnostic("Job init failed : "
  102. + StringUtils.stringifyException(e));
  103. // Leave job in the NEW state. The MR AM will detect that the state is
  104. // not INITED and send a JOB_INIT_FAILED event.
  105. // 返回作业内部状态,JobStateInternal.NEW,即初始化失败后的新建
  106. return JobStateInternal.NEW;
  107. }
  108. }

为了主体逻辑清晰,我们去掉部分细节,保留主干,将作业初始化总结如下:

1、调用setup()方法,完成作业启动前的部分初始化工作,实际上最重要的两件事就是:

1.1、获取并设置作业远程提交路径remoteJobSubmitDir;

1.2、获取并设置作业远程配置文件remoteJobConfFile;

2、调用createSplits()方法,创建分片,并获取任务分片元数据信息TaskSplitMetaInfo数组taskSplitMetaInfo:

通过SplitMetaInfoReader的静态方法readSplitMetaInfo(),从作业远程提交路径remoteJobSubmitDir中读取作业分片元数据信息,也就是每个任务的分片元数据信息,以此确定Map任务数、作业运行方式等一些列后续内容;

3、确定Map Task数目numMapTasks:分片元数据信息数组的长度,即有多少分片就有多少numMapTasks;

4、确定Reduce Task数目numReduceTasks,取作业参数mapreduce.job.reduces,参数未配置默认为0;

5、根据分片元数据信息计算输入长度inputLength,也就是作业大小;

6、根据作业大小inputLength,调用作业的makeUberDecision()方法,决定作业运行模式是Uber模式还是Non-Uber模式:

小作业会通过Uber模式运行,相反,大作业会通过Non-Uber模式运行,可参见《Yarn源码分析之MRAppMaster:作业运行方式Local、Uber、Non-Uber》一文!

7、确定允许Map、Reduce任务失败百分比,取参数mapreduce.map.failures.maxpercent、mapreduce.reduce.failures.maxpercent,参数未配置均默认为0,即不允许Map和Reduce任务失败;

8、创建Map Task;

9、创建Reduce Task;

10、返回作业内部状态,JobStateInternal.INITED,即已经初始化;

11、如果出现异常:

11.1、记录warn级别日志信息:Job init failed,并打印出具体异常;

11.2、返回作业内部状态,JobStateInternal.NEW,即初始化失败后的新建;

未完待续,后续作业初始化部分详细描述、作业启动、作业停止等内容,请关注《Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(二)》

Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(一)的更多相关文章

  1. Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(二)

    本文继<Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(一)>,接着讲述MapReduce作业在MRAppMaster上处理总流程,继上篇讲到作业初始化之后的作 ...

  2. Spark源码分析之一:Job提交运行总流程概述

    Spark是一个基于内存的分布式计算框架,运行在其上的应用程序,按照Action被划分为一个个Job,而Job提交运行的总流程,大致分为两个阶段: 1.Stage划分与提交 (1)Job按照RDD之间 ...

  3. Yarn源码分析之MRAppMaster:作业运行方式Local、Uber、Non-Uber

    基于作业大小因素,MRAppMaster提供了三种作业运行方式:本地Local模式.Uber模式.Non-Uber模式.其中, 1.本地Local模式:通常用于调试: 2.Uber模式:为降低小作业延 ...

  4. Yarn源码分析之如何确定作业运行方式Uber or Non-Uber?

    在MRAppMaster中,当MapReduce作业初始化时,它会通过作业状态机JobImpl中InitTransition的transition()方法,进行MapReduce作业初始化相关操作,而 ...

  5. Android7.0 Phone应用源码分析(三) phone拒接流程分析

    本文主要分析Android拒接电话的流程,下面先来看一下拒接电话流程时序图 步骤1:滑动按钮到拒接图标,会调用到AnswerFragment的onDecline方法 com.android.incal ...

  6. Android7.0 Phone应用源码分析(四) phone挂断流程分析

    电话挂断分为本地挂断和远程挂断,下面我们就针对这两种情况各做分析 先来看下本地挂断电话的时序图: 步骤1:点击通话界面的挂断按钮,会调用到CallCardPresenter的endCallClicke ...

  7. HashMap源码分析(史上最详细的源码分析)

    HashMap简介 HashMap是开发中使用频率最高的用于映射(键值对 key value)处理的数据结构,我们经常把hashMap数据结构叫做散列链表: ObjectI entry<Key, ...

  8. spark 源码分析之二十一 -- Task的执行流程

    引言 在上两篇文章 spark 源码分析之十九 -- DAG的生成和Stage的划分 和 spark 源码分析之二十 -- Stage的提交 中剖析了Spark的DAG的生成,Stage的划分以及St ...

  9. zookeeper源码分析之四服务端(单机)处理请求流程

    上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...

随机推荐

  1. Linux查看系统开机时间(转)

    1.who命令查看 who -b查看最后一次系统启动的时间. who -r查看当前系统运行时间 2.last  reboot last reboot可以看到Linux系统历史启动的时间. 重启一下操作 ...

  2. 微软自家的.Net下的JavaScript引擎——ClearScript

    之前我介绍过一个开源的.Net下的Javascript引擎Javascript .NET,今天发现微软自己也开源了一个JavaScript引擎——ClearScript(当然,也支持VB Script ...

  3. 地图投影与ArcGIS坐标系转换

    1. 通常GIS项目涉及到的坐标系 (1)面向局部区域的大比例尺二维平面:高斯投影(横轴墨卡托) 说明:在市一级的小范围区域的GIS系统,比如规划局.国土局.建设局的系统,大都使用高斯投影,以便与地方 ...

  4. 【jQuery】jquery中 使用$('#parentUid').attr(parentUid);报错jquery-1.11.3.min.js:5 Uncaught TypeError: Cannot read property 'nodeType' of undefined

    jquery中 使用$('#parentUid').attr(parentUid);报错jquery-1.11.3.min.js:5 Uncaught TypeError: Cannot read p ...

  5. 微信php分享页面自定义标题与内容

    1.因为现在分享页面,发给朋友或者朋友圈都没办法自定义标题.图片和内容,所以必须要有微信公众号 2.如果有微信公众号可直接登录,如果没有要注册,注册完或者登录了 3.查看你的权限,左侧最下面开发的接口 ...

  6. 基于CentOS与VmwareStation10搭建Oracle11G RAC 64集群环境:2.搭建环境-2.10.配置用户NTF服务

    2.10.配置用户NTF服务 2.10.1.配置节点RAC1 1) [root@linuxrac1 sysconfig]#sed -i 's/OPTIONS/#OPTIONS/g' /etc/sysc ...

  7. solr6.6 配置拼音分词

    参考:solr6.6 配置同义词 1.下载拼音分析包 下载地址:pinyin.zip 解压后放在core下面的lib文件夹下面: 2.修改managed-schema配置文件 <fieldTyp ...

  8. 二分求幂 - A^B(王道*)

    题目描述: 求A^B的最后三位数表示的整数,说明:A^B的含义是“A的B次方” 输入: 输入数据包含多个测试实例,每个实例占一行,由两个正整数A和B组成(1<=A,B<=10000),如果 ...

  9. vue2自定义事件之$emit

    父组件: API上的解释不多: https://cn.vuejs.org/v2/api/#vm-emit vm.$emit( event, […args] ) 参数: {string} event [ ...

  10. Spring搭配Ehcache实例解析

    转载请注明出处:http://blog.csdn.net/dongdong9223/article/details/50538085 本文出自[我是干勾鱼的博客] 1 Ehcache简单介绍 EhCa ...