zoukankan      html  css  js  c++  java
  • Yarn源码分析之事件异步分发器AsyncDispatcher

     AsyncDispatcher是Yarn中事件异步分发器,它是ResourceManager中的一个基于阻塞队列的分发或者调度事件的组件,其在一个特定的单线程中分派事件,交给AsyncDispatcher中之前注册的针对该事件所属事件类型的事件处理器EventHandler来处理。每个事件类型类可能会有多个处理渠道,即多个事件处理器,可以使用一个线程池调度事件。在Yarn的主节点ResourceManager中,就有一个Dispatcher类型的成员变量rmDispatcher,定义如下:

    [java] view plain copy
     
    1. private Dispatcher rmDispatcher;  

            而rmDispatcher的初始化则在基于AbstractService的ResourceManager服务初始化的serviceInit()方法中,关键代码如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. rmDispatcher = setupDispatcher();  

            继续追踪setupDispatcher()方法,如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. /** 
    2.  * Register the handlers for alwaysOn services 
    3.  */  
    4. private Dispatcher setupDispatcher() {  
    5.   Dispatcher dispatcher = createDispatcher();  
    6.   dispatcher.register(RMFatalEventType.class,  
    7.       new ResourceManager.RMFatalEventDispatcher());  
    8.   return dispatcher;  
    9. }  

            实际上就是通过createDispatcher()方法创建了一个AsyncDispatcher实例,代码如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. protected Dispatcher createDispatcher() {  
    2.   return new AsyncDispatcher();  
    3. }  

            我们先看下AsyncDispatcher的成员变量有哪些,如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. // 待调度处理事件阻塞队列  
    2. private final BlockingQueue<Event> eventQueue;  
    3.   
    4. // AsyncDispatcher是否停止的标志位  
    5. private volatile boolean stopped = false;  
    6.   
    7. // Configuration flag for enabling/disabling draining dispatcher's events on  
    8. // stop functionality.  
    9. // 在stop功能中开启/禁用流尽分发器事件的配置标志位  
    10. private volatile boolean drainEventsOnStop = false;  
    11.   
    12. // Indicates all the remaining dispatcher's events on stop have been drained  
    13. // and processed.  
    14. // stop功能中所有剩余分发器事件已经被处理或流尽的标志位  
    15. private volatile boolean drained = true;  
    16.   
    17. // drained的等待锁  
    18. private Object waitForDrained = new Object();  
    19.   
    20. // For drainEventsOnStop enabled only, block newly coming events into the  
    21. // queue while stopping.  
    22. // 在AsyncDispatcher停止过程中阻塞新近到来的事件进入队列的标志位,仅当drainEventsOnStop启用(即为true)时有效  
    23. private volatile boolean blockNewEvents = false;  
    24.   
    25. // 事件处理器实例  
    26. private EventHandler handlerInstance = null;  
    27.   
    28. // 事件处理调度线程  
    29. private Thread eventHandlingThread;  
    30.   
    31. // 事件类型枚举类Enum到事件处理器EventHandler实例的映射集合  
    32. protected final Map<Class<? extends Enum>, EventHandler> eventDispatchers;  
    33.   
    34. // 标志位:确保调度程序崩溃,但不做系统退出system-exit  
    35. private boolean exitOnDispatchException;  

            AsyncDispatcher中最重要的一个成员变量则是待调度处理事件阻塞队列eventQueue,它是一个阻塞队列,存储的是全部等待调度处理的事件,默认的实现为线程安全的链式阻塞队列LinkedBlockingQueue,这在其无参构造方法中有体现,如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1.  // 无参构造函数  
    2.  public AsyncDispatcher() {  
    3. // 调用有参构造函数,传入线程安全的链式阻塞队列LinkedBlockingQueue实例  
    4.    this(new LinkedBlockingQueue<Event>());  
    5.  }  

            而有参构造函数除了赋值eventQueue外,还会初始化eventDispatchers集合为HashMap,其专门用来存储事件类型枚举类Enum至事件处理器EventHandler实例的映射关系,所有被分发器分发的事件,都必须在按照其所属事件类型在eventDispatchers中注册一个事件处理器EventHandler,等待指定线程调度到该事件后,由其所属事件类型对应的事件处理器EventHandler进行处理,如果不注册事件处理器,则分发器不会对事件进行分发。

            我们上面所说的特定线程就是eventHandlingThread,它是AsyncDispatcher中一个特定的单线程,由其从事件队列中取出事件,并从eventDispatchers中查找事件处理器EventHandler,然后转交EventHandler进行事件的处理。

            AsyncDispatcher中还有一些标志位,如下:

            1、stopped:AsyncDispatcher是否停止的标志位;

            2、drainEventsOnStop:在stop功能中开启/禁用流尽分发器事件的配置标志位,如果启动,则AsyncDispatcher停止前需要先处理完待调度处理事件队列eventQueue中的事件,否则直接停止;

            3、drained:stop功能中所有剩余分发器事件已经被处理或流尽的标志位,为true表示待调度处理事件已处理完,为false则表示尚未处理完;

            4、waitForDrained:标志位drained上的等待锁;

            5、blockNewEvents:在AsyncDispatcher停止过程中阻塞新近到来的事件进入队列的标志位,仅当drainEventsOnStop启用(即为true)时有效;

            6、exitOnDispatchException:调度程序崩溃时是否做系统退出system-exit;

            我们发现,AsyncDispatcher继承自AbstractService,那么它就是Hadoop中的一种抽象服务,其就必须遵循构造实例后先初始化再启动的规则。我们下看下它初始化的serviceInit()方法,如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1.  @Override  
    2.  protected void serviceInit(Configuration conf) throws Exception {  
    3.      
    4. // 取参数yarn.dispatcher.exit-on-error, 参数未配置默认为false   
    5. this.exitOnDispatchException =  
    6.        conf.getBoolean(Dispatcher.DISPATCHER_EXIT_ON_ERROR_KEY,  
    7.          Dispatcher.DEFAULT_DISPATCHER_EXIT_ON_ERROR);  
    8.    super.serviceInit(conf);  
    9.  }  

            除了调用父类的serviceInit()方法设置配置信息成员变量外,它所做的唯一一件事就是确定exitOnDispatchException,取参数yarn.dispatcher.exit-on-error, 参数未配置默认为false。

            做为一个服务,初始化过后就该启动,我们再看其启动serviceStart()方法,如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. @Override  
    2. protected void serviceStart() throws Exception {  
    3.   //start all the components  
    4.   super.serviceStart();  
    5.     
    6.   // 创建事件处理调度线程eventHandlingThread  
    7.   eventHandlingThread = new Thread(createThread());  
    8.   // 设置线程名为AsyncDispatcher event handler  
    9.   eventHandlingThread.setName("AsyncDispatcher event handler");  
    10.   // 启动事件处理调度线程eventHandlingThread  
    11.   eventHandlingThread.start();  
    12. }  

            很简单,创建一个事件处理调度线程eventHandlingThread,设置线程名为"AsyncDispatcher event handler",并启动线程。这个事件处理调度线程eventHandlingThread是通过createThread()来定义的,如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1.   Runnable createThread() {  
    2.     return new Runnable() {  
    3.       @Override  
    4.       public void run() {  
    5.             
    6.         // 标志位stopped为false,即AsyncDispatcher实例未停止的话,且当前线程未中断的话,一直运行  
    7.         while (!stopped && !Thread.currentThread().isInterrupted()) {  
    8.             
    9.           // 判断事件调度队列eventQueue是否为空,并赋值给标志位drained  
    10.           drained = eventQueue.isEmpty();  
    11.             
    12.           // blockNewEvents is only set when dispatcher is draining to stop,  
    13.           // adding this check is to avoid the overhead of acquiring the lock  
    14.           // and calling notify every time in the normal run of the loop.  
    15.             
    16.           // 如果停止过程中阻止新的事件加入待处理队列,即标志位blockNewEvents为true  
    17.           if (blockNewEvents) {  
    18.             synchronized (waitForDrained) {  
    19.             <span style="white-space:pre">  </span>  
    20.               // 如果待处理队列中的事件都已调度完毕,调用waitForDrained的notify()方法通知等待者  
    21.               if (drained) {  
    22.                 waitForDrained.notify();  
    23.               }  
    24.             }  
    25.           }  
    26.             
    27.           Event event;  
    28.           try {  
    29.             // 从事件调度队列eventQueue中取出一个事件  
    30.             // take()方法为取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻塞进入等待状态直到  
    31.             // BlockingQueue有新的数据被加入   
    32.             event = eventQueue.take();  
    33.           } catch(InterruptedException ie) {  
    34.             if (!stopped) {  
    35.               LOG.warn("AsyncDispatcher thread interrupted", ie);  
    36.             }  
    37.             return;  
    38.           }  
    39.             
    40.           // 如果取出待处理事件event,即不为null  
    41.           if (event != null) {  
    42.                 
    43.             // 调用dispatch()方法进行分发  
    44.             dispatch(event);  
    45.           }  
    46.         }  
    47.       }  
    48.     };  
    49.   }  

            我们看下线程的主体逻辑,它的run()方法有一个while循环,标志位stopped为false,即AsyncDispatcher实例未停止的话,且当前线程未中断的话,一直运行,大体如下:

            (一)先处理下特殊情况:

            1、判断事件调度队列eventQueue是否为空,并赋值给标志位drained;

            2、如果停止过程中阻止新的事件加入待处理队列,即标志位blockNewEvents为true,这个标志位为true是在停止服务的serviceStop()方法中,当drainEventsOnStop为true时被设置的,即AsyncDispatcher停止前需要先处理完待调度处理事件队列eventQueue中的事件:

                  2.1如果待处理队列中的事件都已调度完毕(标志位drained为true),调用waitForDrained的notify()方法通知等待者,也就是服务停止serviceStop()方法;

            (二)然后是正常事件调度处理过程:

            1、从事件调度队列eventQueue中取出一个事件:

                  take()方法为取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻塞进入等待状态直到lockingQueue有新的数据被加入;

            2、如果取出待处理事件event,即不为null,调用dispatch()方法进行分发:

                  2.1、根据事件event获取事件类型枚举类type;

                  2.2、根据事件类型枚举类type,从eventDispatchers中获取事件处理器EventHandler实例handler;

                  2.3、如果handler不为空,调用handler的handle()方法处理事件event,否则抛出异常,提示针对事件类型type的事件处理器handler没有注册;

            而当线程遇到InterruptedException异常时,即外部中断该线程时,如果stopped标志位为false,非AsyncDispatcher服务正常停止情况下的中断,则记录warn级别日志信息,最后统一返回。

            上面提到的dispatch()方法代码如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. @SuppressWarnings("unchecked")  
    2. protected void dispatch(Event event) {  
    3.   //all events go thru this loop  
    4.   if (LOG.isDebugEnabled()) {  
    5.     LOG.debug("Dispatching the event " + event.getClass().getName() + "."  
    6.         + event.toString());  
    7.   }  
    8.   
    9.   // 根据事件event获取事件类型枚举类type  
    10.   Class<? extends Enum> type = event.getType().getDeclaringClass();  
    11.   
    12.   try{  
    13.       
    14.     // 根据事件类型枚举类type,从eventDispatchers中获取事件处理器EventHandler实例handler  
    15.     EventHandler handler = eventDispatchers.get(type);  
    16.     if(handler != null) {  
    17.     // 如果handler不为空,调用handler的handle()方法处理事件event  
    18.       handler.handle(event);  
    19.     } else {  
    20.     // 否则抛出异常,提示针对事件类型type的事件处理器handler没有注册  
    21.       throw new Exception("No handler for registered for " + type);  
    22.     }  
    23.   } catch (Throwable t) {  
    24.     //TODO Maybe log the state of the queue  
    25.     LOG.fatal("Error in dispatcher thread", t);  
    26.     // If serviceStop is called, we should exit this thread gracefully.  
    27.     if (exitOnDispatchException  
    28.         && (ShutdownHookManager.get().isShutdownInProgress()) == false  
    29.         && stopped == false) {  
    30.       LOG.info("Exiting, bbye..");  
    31.       System.exit(-1);  
    32.     }  
    33.   }  
    34. }  

            dispatch()方法还有一部分,就是当异常Throwable发生时的处理。正常情况下,如果是正常调用serviceStop()方法停止服务,那么当前线程应该优雅的退出,而这里,如果发生了异常,同时exitOnDispatchException配置为true,即发生异常时退出系统,且stopped为false,不是通过服务停止发生的异常,那么,系统非正常退出,System.exit(-1)。

            以上就是整个AsyncDispatcher服务构造、初始化、启动、并处理的主要内容,下面我们再看下其服务停止方面的内容,serviceStop()方法如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1.  @Override  
    2.  protected void serviceStop() throws Exception {  
    3.      
    4. // 如果标志位drainEventsOnStop为true,则    
    5. if (drainEventsOnStop) {  
    6.       
    7.   // 标志位blockNewEvents设置为true,阻止新的事件被加入待处理队列  
    8.      blockNewEvents = true;  
    9.        
    10.      // 记录info级别Log信息  
    11.      LOG.info("AsyncDispatcher is draining to stop, igonring any new events.");  
    12.        
    13.      // waitForDrained上通过synchronized进行同步:  
    14.      synchronized (waitForDrained) {  
    15.         
    16.     // 如果队列中的事件还没有处理完(drained为false),同时事件处理调度线程eventHandlingThread仍然存活  
    17.        while (!drained && eventHandlingThread.isAlive()) {  
    18.           
    19.          // waitForDrained释放锁,等待1s  
    20.          waitForDrained.wait(1000);  
    21.          LOG.info("Waiting for AsyncDispatcher to drain.");  
    22.        }  
    23.      }  
    24.    }  
    25.      
    26.    // 停止标志位stopped设置为true  
    27.    stopped = true;  
    28.      
    29.    // 如果事件处理调度线程eventHandlingThread不为null,  
    30.    if (eventHandlingThread != null) {  
    31.        
    32.      // 中断eventHandlingThread线程  
    33.      eventHandlingThread.interrupt();  
    34.      try {  
    35.         
    36.     // 等待eventHandlingThread线程结束  
    37.        eventHandlingThread.join();  
    38.      } catch (InterruptedException ie) {  
    39.        LOG.warn("Interrupted Exception while stopping", ie);  
    40.      }  
    41.    }  
    42.   
    43.    // stop all the components  
    44.    super.serviceStop();  
    45.  }  

            服务停止时的处理流程如下:
            1、如果标志位drainEventsOnStop为true,即AsyncDispatcher停止前需要先处理完待调度处理事件队列eventQueue中的事件,则:

                  1.1、标志位blockNewEvents设置为true,阻止新的事件被加入待处理队列;

                  1.2、记录info级别Log信息--AsyncDispatcher is draining to stop, igonring any new events;

                  1.3、waitForDrained上通过synchronized进行同步:如果队列中的事件还没有处理完(drained为false),同时事件处理调度线程eventHandlingThread仍然存活,waitForDrained释放锁,等待1s,并记录info级别Log信息--Waiting for AsyncDispatcher to drain,这里的wait其实是等待事件处理调度线程eventHandlingThread调度完eventQueue队列中的剩余事件,见上面线程run()方法解释;

            2、停止标志位stopped设置为true,标志AsyncDispatcher服务已停止;

            3、如果事件处理调度线程eventHandlingThread不为null,中断eventHandlingThread线程,上面也有对中断异常的处理;

            4、等待eventHandlingThread线程结束;

            5、调用父类AbstractService的serviceStop()方法(其实是个空方法)。

            等等,上面说了那么多,是不是少了些什么?AsyncDispatcher是不是很像一个生产者消费者模型?通过上面看来,eventHandlingThread事件处理调度线程不断的从待调度处理事件队列eventQueue中取出事件进行处理的过程,就是消费过程。那么生产过程是怎样的呢?下面我们就这个问题展开分析。

            正常的分析过程应该是看看哪些地方如何使用的AsyncDispatcher。这里,我们根据AsyncDispatcher中eventQueue的private特性,看看AsyncDispatcher中都有哪些地方会将事件加入到eventQueue队列,答案很明显而且是唯一的,在GenericEventHandler中handle()方法会通过eventQueue.put(event)往队列中添加数据,即所谓的生产过程。那么,这个GenericEventHandler是如何获得的呢?getEventHandler()方法告诉了我们答案,如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. @Override  
    2. public EventHandler getEventHandler() {  
    3.   if (handlerInstance == null) {  
    4.     handlerInstance = new GenericEventHandler();  
    5.   }  
    6.   return handlerInstance;  
    7. }  

            成员变量handlerInstance如果为空,构造一个GenericEventHandler实例赋值给成员变量handlerInstance,并返回,下次再获取的话,就可以直接返回handlerInstance了。而这个handlerInstance在AsyncDispatcher内部也是一个私有的事件处理器实例,仅仅在第一次调用getEventHandler()方法时才会完成初始化。现在你也会不会困惑上面我们为什么没有讲解handlerInstance,这里我们可以简单的归纳下:

            handlerInstance是AsyncDispatcher内部一个私有的事件处理器实例,它负责处理添加待调度处理事件这一事件,虽然感觉说法有些拗口,但是AsyncDispatcher很聪明的将添加待调度处理事件这一生产过程也当作一个事件来处理,我们看下GenericEventHandler定义及其handle()方法,如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. class GenericEventHandler implements EventHandler<Event> {  
    2.   public void handle(Event event) {  
    3.       
    4.     // 如果blockNewEvents为true,即AsyncDispatcher服务停止过程正在发生,  
    5.     // 且阻止新的事件加入待调度处理事件队列eventQueue,直接返回  
    6.     if (blockNewEvents) {  
    7.       return;  
    8.     }  
    9.       
    10.     // 标志位drained设置为false,说明队列中尚有事件需要调度  
    11.     drained = false;  
    12.   
    13.     /* all this method does is enqueue all the events onto the queue */  
    14.       
    15.     // 获取队列eventQueue大小qSize  
    16.     int qSize = eventQueue.size();  
    17.       
    18.     // 每隔1000记录一条info级别日志信息,比如:Size of event-queue is 2000  
    19.     if (qSize !=0 && qSize %1000 == 0) {  
    20.       LOG.info("Size of event-queue is " + qSize);  
    21.     }  
    22.       
    23.     // 获取队列eventQueue剩余容量remCapacity  
    24.     int remCapacity = eventQueue.remainingCapacity();  
    25.       
    26.     // 如果剩余容量remCapacity小于1000,记录warn级别日志信息,比如:Very low remaining capacity in the event-queue:  888  
    27.     if (remCapacity < 1000) {  
    28.       LOG.warn("Very low remaining capacity in the event-queue: "  
    29.           + remCapacity);  
    30.     }  
    31.       
    32.     // 队列eventQueue中添加事件event  
    33.     try {  
    34.       eventQueue.put(event);  
    35.     } catch (InterruptedException e) {  
    36.       if (!stopped) {  
    37.         LOG.warn("AsyncDispatcher thread interrupted", e);  
    38.       }  
    39.       throw new YarnRuntimeException(e);  
    40.     }  
    41.   };  
    42. }  

            handle()方法的处理逻辑如下:

            1、如果blockNewEvents为true,即AsyncDispatcher服务停止过程正在发生,且阻止新的事件加入待调度处理事件队列eventQueue,直接返回;

            2、标志位drained设置为false,说明队列中尚有事件需要调度;

            3、获取队列eventQueue大小qSize,每隔1000记录一条info级别日志信息,比如:Size of event-queue is 2000;

            4、获取队列eventQueue剩余容量remCapacity,如果剩余容量remCapacity小于1000,记录warn级别日志信息,比如:Very low remaining capacity in the event-queue:  888;

            5、队列eventQueue中添加事件event。

            上面,我们讲了AsyncDispatcher作为一个Dispatcher接口实现者所实现的getEventHandler()方法了,下面我们再讲接口另外一个重要方法的实现,register()方法,即将指定事件类型枚举类eventType与事件处理器EventHandler实例的映射关系,注册到AsyncDispatcher的eventDispatchers集合,代码如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1.  @SuppressWarnings("unchecked")  
    2.  @Override  
    3.  public void register(Class<? extends Enum> eventType,  
    4.      EventHandler handler) {  
    5.    /* check to see if we have a listener registered */  
    6.      
    7. // 查看事件类型eventType对应的事件处理器EventHandler之前是否在eventDispatchers中注册过  
    8. EventHandler<Event> registeredHandler = (EventHandler<Event>)  
    9.    eventDispatchers.get(eventType);  
    10.    LOG.info("Registering " + eventType + " for " + handler.getClass());  
    11.      
    12.    if (registeredHandler == null) {// 没有注册过  
    13.      eventDispatchers.put(eventType, handler);  
    14.    } else if (!(registeredHandler instanceof MultiListenerHandler)){// 已经注册过,且不是MultiListenerHandler  
    15.      /* for multiple listeners of an event add the multiple listener handler */  
    16.        
    17.      // 构造一个MultiListenerHandler实例,将之前注册过的事件处理器registeredHandler连同这次需要注册的事件处理器handler,  
    18.     // 做为一个符合监听事件处理器注册到eventDispatchers  
    19.      MultiListenerHandler multiHandler = new MultiListenerHandler();  
    20.      multiHandler.addHandler(registeredHandler);  
    21.      multiHandler.addHandler(handler);  
    22.      eventDispatchers.put(eventType, multiHandler);  
    23.    } else {// 已经注册过,且是MultiListenerHandler  
    24.      /* already a multilistener, just add to it */  
    25.        
    26.      // 已经是一个MultiListenerHandler的话,强制转换下  
    27.      MultiListenerHandler multiHandler  
    28.      = (MultiListenerHandler) registeredHandler;  
    29.        
    30.      // 直接追加注册新的handler  
    31.      multiHandler.addHandler(handler);  
    32.    }  
    33.  }  

            其实register()的方法逻辑很简单,总结如下:

            1、查看事件类型eventType对应的事件处理器EventHandler之前是否在eventDispatchers中注册过,取出赋值给registeredHandler;

            2、判断registeredHandler:

                  2.1、如果没有注册过,直接放入eventDispatchers进行注册;

                  2.2、如果已经注册过,且不是MultiListenerHandler:构造一个MultiListenerHandler实例,将之前注册过的事件处理器registeredHandler连同这次需要注册的事件处理器handler,做为一个多路复合监听事件处理器注册到eventDispatchers;

                  2.3、如果已经注册过,且是MultiListenerHandler:强制转换下,直接追加注册新的handler。

            最后,我们再讲解下这个多路复合监听事件处理器MultiListenerHandler,它是AsyncDispatcher的内部类,也实现了EventHandler<Event>接口,其构造方法如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. public MultiListenerHandler() {  
    2.   listofHandlers = new ArrayList<EventHandler<Event>>();  
    3. }  

            初始化处理器列表listofHandlers。

            它的addHandler()方法就是将需要注册的新的事件处理器追加到listofHandlers列表中,代码如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. void addHandler(EventHandler<Event> handler) {  
    2.   listofHandlers.add(handler);  
    3. }  

            而作为一个事件处理器核心功能实现的handle()方法,如下:

    [java] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. @Override  
    2. public void handle(Event event) {  
    3.   for (EventHandler<Event> handler: listofHandlers) {  
    4.     handler.handle(event);  
    5.   }  
    6. }  

            依次有序遍历listofHandlers中的事件处理器,分别调用它们的handle方法进行事件处理,真正实现了它所设计的Multiplexing an event目标,将事件依次通过列表中的handler的处理。

  • 相关阅读:
    SQL 数据库 复制 与订阅 实现数据同步
    SQL 2008配置管理工具服务显示 远程过程调用失败0x800706be
    SQL2005中使用identity_insert向自动增量字段中写入内
    【树莓派】【转载】基于树莓派,制作家庭媒体中心+下载机
    Linux 按时间批量删除文件(删除N天前文件)
    【树莓派】为树莓派配置或扩展swap分区
    开源硬件相关平台
    【树莓派】树莓派上刷android系统
    【树莓派】树莓派上面安装配置teamviewer
    【树莓派】使用xdrp远程登录树莓派的图形界面
  • 原文地址:https://www.cnblogs.com/jirimutu01/p/5556405.html
Copyright © 2011-2022 走看看