MyDisruptor V5版本介绍

在v4版本的MyDisruptor实现多线程生产者后。按照计划,v5版本的MyDisruptor需要支持更便于用户使用的DSL风格的API。

由于该文属于系列博客的一部分,需要先对之前的博客内容有所了解才能更好地理解本篇博客

为什么Disruptor需要DSL风格的API

通过前面4个版本的迭代,MyDisruptor已经实现了disruptor的大多数功能。但对程序可读性有要求的读者可能会注意到,之前给出的demo示例代码中对于构建多个消费者之间的依赖关系时细节有点多。

构建一个有上游消费者依赖的EventProcessor消费者一般来说需要通过以下几步完成:

  1. 获得所要依赖的上游消费者序列集合,并在创建EventProcessor时通过参数传入
  2. 获得所创建的EventProcessor对应的消费者序列对象
  3. 将获得的消费者序列对象注册到RingBuffer中
  4. 通过线程池或者start等方式启动EventProcessor线程,开始监听消费

目前的版本中,每创建一个消费者都需要写一遍上述的模板代码。对于理解Disruptor原理的人来说还勉强能接受,但还是很繁琐且容易在细节上犯错,更遑论对disruptor底层不大了解的普通用户。

基于上述原因,disruptor提供了更加简单易用的DSL风格API,使得对disruptor底层各组件间交互不甚了解的用户也能很方便的使用disruptor,去构建不同消费者组间的依赖关系。

什么是DSL风格的API?

DSL即Domain Specific Language,领域特定语言。DSL是针对特定领域抽象出的一个特定语言,通过进一层的抽象来代替大量繁琐的通用代码段,如sql、shell等都是常见的dsl。

而DSL风格的API最大的特点就是接口的定义贴合业务场景,因此易于理解和使用。

MyDisruptor DSL风格API实现详解

Disruptor

首先要介绍的就是Disruptor类,disruptor类主要用于创建一个符合用户需求的RingBuffer,并提供一组易用的api以屏蔽底层组件交互的细节。

MyDisruptor类的构造函数有五个参数,分别是:

  1. 用户自定义的事件生产器(EventFactory)
  2. RingBuffer的容量大小
  3. 消费者执行器(juc的Executor实现类)
  4. 生产者类型枚举(指定单线程生产者 or 多线程生产者)
  5. 消费者阻塞策略实现(WaitStrategy)

以上都是需要用户自定义或者指定的核心参数,构建好的disruptor的同时,也生成了RingBuffer和指定类型的生产者序列器。

/**
* disruptor dsl(仿Disruptor.Disruptor)
* */
public class MyDisruptor<T> { private final MyRingBuffer<T> ringBuffer;
private final Executor executor;
private final MyConsumerRepository<T> consumerRepository = new MyConsumerRepository<>();
private final AtomicBoolean started = new AtomicBoolean(false); public MyDisruptor(
final MyEventFactory<T> eventProducer,
final int ringBufferSize,
final Executor executor,
final ProducerType producerType,
final MyWaitStrategy myWaitStrategy) { this.ringBuffer = MyRingBuffer.create(producerType,eventProducer,ringBufferSize,myWaitStrategy);
this.executor = executor;
} /**
* 获得当亲Disruptor的ringBuffer
* */
public MyRingBuffer<T> getRingBuffer() {
return ringBuffer;
} // 注意:省略了大量无关代码
}
EventHandlerGroup

创建好Disruptor后,便可以按照需求编排各种消费者的依赖逻辑了。创建消费者时除了用户自定义的消费量逻辑接口(EventHandler/WorkHandler),还有两个关键要素需要指定,一是指定是单线程生产者还是多线程,二是指定当前消费者的上游消费者序列集合(或者没有)。

两两组合四种情况,为此Disruptor类一共提供了四个方法用于创建消费者:

  1. handleEventsWith(创建无上游消费者依赖的单线程消费者)
  2. createEventProcessors(创建有上游消费者依赖的单线程消费者)
  3. handleEventsWithWorkerPool(创建无上游消费者依赖的多线程消费者)
  4. createWorkerPool(创建有上游消费者依赖的多线程消费者)

这四个方法的返回值都是EventHandlerGroup对象,其中提供了关键的then/thenHandleEventsWithWorkerPool方法用来链式的编排多个消费者组。

实际上disruptor中的EventHandlerGroup还提供了等更多的dsl风格的方法(如and),限于篇幅MyDisruptor中只实现了最关键的几个方法。

/**
* DSL事件处理器组(仿Disruptor.EventHandlerGroup)
* */
public class MyEventHandlerGroup<T> { private final MyDisruptor<T> disruptor;
private final MyConsumerRepository<T> myConsumerRepository;
private final MySequence[] sequences; public MyEventHandlerGroup(MyDisruptor<T> disruptor,
MyConsumerRepository<T> myConsumerRepository,
MySequence[] sequences) {
this.disruptor = disruptor;
this.myConsumerRepository = myConsumerRepository;
this.sequences = sequences;
} @SafeVarargs
public final MyEventHandlerGroup<T> then(final MyEventHandler<T>... myEventHandlers) {
return handleEventsWith(myEventHandlers);
} @SafeVarargs
public final MyEventHandlerGroup<T> handleEventsWith(final MyEventHandler<T>... handlers) {
return disruptor.createEventProcessors(sequences, handlers);
} @SafeVarargs
public final MyEventHandlerGroup<T> thenHandleEventsWithWorkerPool(final MyWorkHandler<T>... handlers) {
return handleEventsWithWorkerPool(handlers);
} @SafeVarargs
public final MyEventHandlerGroup<T> handleEventsWithWorkerPool(final MyWorkHandler<T>... handlers) {
return disruptor.createWorkerPool(sequences, handlers);
}
}
MyDisruptor完整代码
/**
* disruptor dsl(仿Disruptor.Disruptor)
* */
public class MyDisruptor<T> { private final MyRingBuffer<T> ringBuffer;
private final Executor executor;
private final MyConsumerRepository<T> consumerRepository = new MyConsumerRepository<>();
private final AtomicBoolean started = new AtomicBoolean(false); public MyDisruptor(
final MyEventFactory<T> eventProducer,
final int ringBufferSize,
final Executor executor,
final ProducerType producerType,
final MyWaitStrategy myWaitStrategy) { this.ringBuffer = MyRingBuffer.create(producerType,eventProducer,ringBufferSize,myWaitStrategy);
this.executor = executor;
} /**
* 注册单线程消费者 (无上游依赖消费者,仅依赖生产者序列)
* */
@SafeVarargs
public final MyEventHandlerGroup<T> handleEventsWith(final MyEventHandler<T>... myEventHandlers){
return createEventProcessors(new MySequence[0], myEventHandlers);
} /**
* 注册单线程消费者 (有上游依赖消费者,仅依赖生产者序列)
* @param barrierSequences 依赖的序列屏障
* @param myEventHandlers 用户自定义的事件消费者集合
* */
public MyEventHandlerGroup<T> createEventProcessors(
final MySequence[] barrierSequences,
final MyEventHandler<T>[] myEventHandlers) { final MySequence[] processorSequences = new MySequence[myEventHandlers.length];
final MySequenceBarrier barrier = ringBuffer.newBarrier(barrierSequences); int i=0;
for(MyEventHandler<T> myEventConsumer : myEventHandlers){
final MyBatchEventProcessor<T> batchEventProcessor =
new MyBatchEventProcessor<>(ringBuffer, myEventConsumer, barrier); processorSequences[i] = batchEventProcessor.getCurrentConsumeSequence();
i++; // consumer对象都维护起来,便于后续start时启动
consumerRepository.add(batchEventProcessor);
} // 更新当前生产者注册的消费者序列
updateGatingSequencesForNextInChain(barrierSequences,processorSequences); return new MyEventHandlerGroup<>(this,this.consumerRepository,processorSequences);
} /**
* 注册多线程消费者 (无上游依赖消费者,仅依赖生产者序列)
* */
@SafeVarargs
public final MyEventHandlerGroup<T> handleEventsWithWorkerPool(final MyWorkHandler<T>... myWorkHandlers) {
return createWorkerPool(new MySequence[0], myWorkHandlers);
} /**
* 注册多线程消费者 (有上游依赖消费者,仅依赖生产者序列)
* @param barrierSequences 依赖的序列屏障
* @param myWorkHandlers 用户自定义的事件消费者集合
* */
public MyEventHandlerGroup<T> createWorkerPool(
final MySequence[] barrierSequences, final MyWorkHandler<T>[] myWorkHandlers) {
final MySequenceBarrier sequenceBarrier = ringBuffer.newBarrier(barrierSequences);
final MyWorkerPool<T> workerPool = new MyWorkerPool<>(ringBuffer, sequenceBarrier, myWorkHandlers); // consumer都保存起来,便于start启动
consumerRepository.add(workerPool); final MySequence[] workerSequences = workerPool.getCurrentWorkerSequences(); updateGatingSequencesForNextInChain(barrierSequences, workerSequences); return new MyEventHandlerGroup<>(this, consumerRepository,workerSequences);
} private void updateGatingSequencesForNextInChain(final MySequence[] barrierSequences, final MySequence[] processorSequences) {
if (processorSequences.length != 0) {
// 这是一个优化操作:
// 由于新的消费者通过ringBuffer.newBarrier(barrierSequences),已经是依赖于之前ringBuffer中已有的消费者序列
// 消费者即EventProcessor内部已经设置好了老的barrierSequences为依赖,因此可以将ringBuffer中已有的消费者序列去掉
// 只需要保存,依赖当前消费者链条最末端的序列即可(也就是最慢的序列),这样生产者可以更快的遍历注册的消费者序列
for(MySequence sequenceV4 : barrierSequences){
ringBuffer.removeConsumerSequence(sequenceV4);
}
for(MySequence sequenceV4 : processorSequences){
// 新设置的就是当前消费者链条最末端的序列
ringBuffer.addConsumerSequence(sequenceV4);
}
}
} /**
* 启动所有已注册的消费者
* */
public void start(){
// cas设置启动标识,避免重复启动
if (!started.compareAndSet(false, true)) {
throw new IllegalStateException("Disruptor只能启动一次");
} // 遍历所有的消费者,挨个start启动
this.consumerRepository.getConsumerInfos().forEach(
item->item.start(this.executor)
);
} /**
* 获得当亲Disruptor的ringBuffer
* */
public MyRingBuffer<T> getRingBuffer() {
return ringBuffer;
}
}
Disruptor内部消费者依赖编排的性能小优化
  • 在上面完整的MyDisruptor实现中可以看到,在每次构建消费者后都执行了updateGatingSequencesForNextInChain这个方法。方法中将当前消费者序列号注册进RingBuffer的同时,还将传入的上游barrierSequence集合从当前RingBuffer中移除。

    这样做主要是为了提高生产者在获取当前最慢消费者时的性能。
  • 在没有这个优化之前,所有的消费者的序列号都会被注册到RingBuffer中,而生产者通过getMinimumSequence方法遍历所有注册的消费者序列集合获得其中最小的序列值(最慢的消费者)。
  • 我们知道,通过Disruptor的DSL接口创建的消费者之间是存在依赖关系的,每个消费者的实现内部保证了其自身的序列号不会超过上游的消费者序列。所以在存在上下游依赖关系的、所有消费者序列的集合中,最慢的消费者必然是处于下游的消费者序列号。

    所以在RingBuffer中就可以不再维护更上游的消费者序列号,从而加快getMinimumSequence方法中遍历数组的速度。

MyDisruptorV5版本demo示例

下面通过一个简单但不失一般性的示例,来展示一下DSL风格API到底简化了多少复杂度。

不使用DSL风格API的示例

public class MyRingBufferV5DemoOrginal {
/**
* 消费者依赖关系图(简单起见都是单线程消费者):
* A -> BC -> D
* -> E -> F
* */
public static void main(String[] args) {
// 环形队列容量为16(2的4次方)
int ringBufferSize = 16; // 创建环形队列
MyRingBuffer<OrderEventModel> myRingBuffer = MyRingBuffer.createSingleProducer(
new OrderEventProducer(), ringBufferSize, new MyBlockingWaitStrategy()); // 获得ringBuffer的序列屏障(最上游的序列屏障内只维护生产者的序列)
MySequenceBarrier mySequenceBarrier = myRingBuffer.newBarrier(); // ================================== 基于生产者序列屏障,创建消费者A
MyBatchEventProcessor<OrderEventModel> eventProcessorA =
new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerA"), mySequenceBarrier);
MySequence consumeSequenceA = eventProcessorA.getCurrentConsumeSequence();
// RingBuffer监听消费者A的序列
myRingBuffer.addGatingConsumerSequenceList(consumeSequenceA); // ================================== 通过消费者A的序列号创建序列屏障(构成消费的顺序依赖),创建消费者B
MySequenceBarrier mySequenceBarrierB = myRingBuffer.newBarrier(consumeSequenceA); MyBatchEventProcessor<OrderEventModel> eventProcessorB =
new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerB"), mySequenceBarrierB);
MySequence consumeSequenceB = eventProcessorB.getCurrentConsumeSequence();
// RingBuffer监听消费者B的序列
myRingBuffer.addGatingConsumerSequenceList(consumeSequenceB); // ================================== 通过消费者A的序列号创建序列屏障(构成消费的顺序依赖),创建消费者C
MySequenceBarrier mySequenceBarrierC = myRingBuffer.newBarrier(consumeSequenceA); MyBatchEventProcessor<OrderEventModel> eventProcessorC =
new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerC"), mySequenceBarrierC);
MySequence consumeSequenceC = eventProcessorC.getCurrentConsumeSequence();
// RingBuffer监听消费者C的序列
myRingBuffer.addGatingConsumerSequenceList(consumeSequenceC); // ================================== 消费者D依赖上游的消费者B,C,通过消费者B、C的序列号创建序列屏障(构成消费的顺序依赖)
MySequenceBarrier mySequenceBarrierD = myRingBuffer.newBarrier(consumeSequenceB,consumeSequenceC);
// 基于序列屏障,创建消费者D
MyBatchEventProcessor<OrderEventModel> eventProcessorD =
new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerD"), mySequenceBarrierD);
MySequence consumeSequenceD = eventProcessorD.getCurrentConsumeSequence();
// RingBuffer监听消费者D的序列
myRingBuffer.addGatingConsumerSequenceList(consumeSequenceD); // ================================== 通过消费者A的序列号创建序列屏障(构成消费的顺序依赖),创建消费者E
MySequenceBarrier mySequenceBarrierE = myRingBuffer.newBarrier(consumeSequenceA); MyBatchEventProcessor<OrderEventModel> eventProcessorE =
new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerE"), mySequenceBarrierE);
MySequence consumeSequenceE = eventProcessorE.getCurrentConsumeSequence();
// RingBuffer监听消费者E的序列
myRingBuffer.addGatingConsumerSequenceList(consumeSequenceE); // ================================== 通过消费者E的序列号创建序列屏障(构成消费的顺序依赖),创建消费者F
MySequenceBarrier mySequenceBarrierF = myRingBuffer.newBarrier(consumeSequenceE); MyBatchEventProcessor<OrderEventModel> eventProcessorF =
new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerF"), mySequenceBarrierF);
MySequence consumeSequenceF = eventProcessorF.getCurrentConsumeSequence();
// RingBuffer监听消费者F的序列
myRingBuffer.addGatingConsumerSequenceList(consumeSequenceF); Executor executor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
// 启动消费者线程A
executor.execute(eventProcessorA);
// 启动消费者线程B
executor.execute(eventProcessorB);
// 启动消费者线程C
executor.execute(eventProcessorC);
// 启动消费者线程D
executor.execute(eventProcessorD);
// 启动消费者线程E
executor.execute(eventProcessorE);
// 启动消费者线程F
executor.execute(eventProcessorF); // 生产者发布100个事件
for(int i=0; i<100; i++) {
long nextIndex = myRingBuffer.next();
OrderEventModel orderEvent = myRingBuffer.get(nextIndex);
orderEvent.setMessage("message-"+i);
orderEvent.setPrice(i * 10);
System.out.println("生产者发布事件:" + orderEvent);
myRingBuffer.publish(nextIndex);
}
}
}

使用DSL风格APi的示例

public class MyRingBufferV5DemoUseDSL {

    /**
* 消费者依赖关系图(简单起见都是单线程消费者):
* A -> BC -> D
* -> E -> F
* */
public static void main(String[] args) {
// 环形队列容量为16(2的4次方)
int ringBufferSize = 16; MyDisruptor<OrderEventModel> myDisruptor = new MyDisruptor<>(
new OrderEventProducer(), ringBufferSize,
new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new SynchronousQueue<>()),
ProducerType.SINGLE,
new MyBlockingWaitStrategy()
); MyEventHandlerGroup<OrderEventModel> hasAHandlerGroup = myDisruptor.handleEventsWith(new OrderEventHandlerDemo("consumerA")); hasAHandlerGroup.then(new OrderEventHandlerDemo("consumerB"),new OrderEventHandlerDemo("consumerC"))
.then(new OrderEventHandlerDemo("consumerD")); hasAHandlerGroup.then(new OrderEventHandlerDemo("consumerE"))
.then(new OrderEventHandlerDemo("consumerF"));
// 启动disruptor中注册的所有消费者
myDisruptor.start(); MyRingBuffer<OrderEventModel> myRingBuffer = myDisruptor.getRingBuffer();
// 生产者发布100个事件
for(int i=0; i<100; i++) {
long nextIndex = myRingBuffer.next();
OrderEventModel orderEvent = myRingBuffer.get(nextIndex);
orderEvent.setMessage("message-"+i);
orderEvent.setPrice(i * 10);
System.out.println("生产者发布事件:" + orderEvent);
myRingBuffer.publish(nextIndex);
}
}
}
  • 可以看到实现同样的业务逻辑时,使用DSL风格的API由于减少了大量的模板代码,代码量大幅减少的同时还增强了程序的可读性。这证明了disruptor的DSL风格API设计是很成功的。

总结

  • 本篇博客介绍了Disruptor的DSL风格的API最核心的实现逻辑,并且通过对比展示了相同业务下DSL风格的API简单易理解的特点。
  • 限于篇幅,自己实现的MyDisruptor中并没有将disruptor中DSL风格的API功能全部实现,而仅仅实现了最常用、最核心的一部分。

    感兴趣的读者可以在理解当前v5版本MyDisruptor的基础之上,通过阅读disruptor的源码做进一步了解。
  • 目前v5版本的MyDisruptor已经实现了disruptor的绝大多数功能,最后的v6版本中将会对MyDisruptor中已有的缺陷进行进一步的优化。

    v6版本的MyDisruptor将会解决伪共享、优雅终止等关键问题并进行对应原理的解析,敬请期待。

disruptor无论在整体设计还是最终代码实现上都有很多值得反复琢磨和学习的细节,希望能帮助到对disruptor感兴趣的小伙伴。

本篇博客的完整代码在我的github上:https://github.com/1399852153/MyDisruptor 分支:feature/lab5

从零开始实现lmax-Disruptor队列(五)Disruptor DSL风格API原理解析的更多相关文章

  1. 大数据学习day24-------spark07-----1. sortBy是Transformation算子,为什么会触发Action 2. SparkSQL 3. DataFrame的创建 4. DSL风格API语法 5 两种风格(SQL、DSL)计算workcount案例

    1. sortBy是Transformation算子,为什么会触发Action sortBy需要对数据进行全局排序,其需要用到RangePartitioner,而在创建RangePartitioner ...

  2. 从零开始实现lmax-Disruptor队列(六)Disruptor 解决伪共享、消费者优雅停止实现原理解析

    MyDisruptor V6版本介绍 在v5版本的MyDisruptor实现DSL风格的API后.按照计划,v6版本的MyDisruptor作为最后一个版本,需要对MyDisruptor进行最终的一些 ...

  3. 高性能环形队列框架 Disruptor 核心概念

    高性能环形队列框架 Disruptor Disruptor 是英国外汇交易公司LMAX开发的一款高吞吐低延迟内存队列框架,其充分考虑了底层CPU等运行模式来进行数据结构设计 (mechanical s ...

  4. 从零开始实现lmax-Disruptor队列(一)RingBuffer与单生产者、单消费者工作原理解析

    1.lmax-Disruptor队列介绍 disruptor是英国著名的金融交易所lmax旗下技术团队开发的一款java实现的高性能内存队列框架 其发明disruptor的主要目的是为了改进传统的内存 ...

  5. 从零开始实现lmax-Disruptor队列(二)多消费者、消费者组间消费依赖原理解析

    MyDisruptor V2版本介绍 在v1版本的MyDisruptor实现单生产者.单消费者功能后.按照计划,v2版本的MyDisruptor需要支持多消费者和允许设置消费者组间的依赖关系. 由于该 ...

  6. 从零开始实现lmax-Disruptor队列(三)多线程消费者WorkerPool原理解析

    MyDisruptor V3版本介绍 在v2版本的MyDisruptor实现多消费者.消费者组间依赖功能后.按照计划,v3版本的MyDisruptor需要支持多线程消费者的功能. 由于该文属于系列博客 ...

  7. 从零开始实现lmax-Disruptor队列(四)多线程生产者MultiProducerSequencer原理解析

    MyDisruptor V4版本介绍 在v3版本的MyDisruptor实现多线程消费者后.按照计划,v4版本的MyDisruptor需要支持线程安全的多线程生产者功能. 由于该文属于系列博客的一部分 ...

  8. java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析

    java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java ...

  9. 学习笔记:CentOS7学习之十五: RAID磁盘阵列的原理与搭建

    目录 学习笔记:CentOS7学习之十五: RAID磁盘阵列的原理与搭建 14.1 RAID概念 14.1.1 RAID几种常见的类型 14.1.2 RAID-0工作原理 14.1.3 RAID-1工 ...

随机推荐

  1. SpringMVC 配置 & 初识 & 注解 &重定向与转发

    初识 在web.xml 中注册DispatcherServlet <servlet> <servlet-name>springmvc</servlet-name> ...

  2. /proc/meminfo 解释

  3. C#开发PACS医学影像三维重建(十三):基于人体CT值从皮肤渐变到骨骼的梯度透明思路

    当我们将CT切片重建为三维体之后,通常会消除一些不必要的外部组织来观察内部病灶, 一般思路是根据人体常见CT值范围来使得部分组织透明来达到效果, 但这是非黑即白的,即,要么显示皮肤,要么显示神经,要么 ...

  4. NMS技术总结(NMS原理、多类别NMS、NMS的缺陷、NMS的改进思路、各种NMS方法)

    ​  前言  本文介绍了NMS的应用场合.基本原理.多类别NMS方法和实践代码.NMS的缺陷和改进思路.介绍了改进NMS的几种常用方法.提供了其它不常用的方法的链接. 本文很早以前发过,有个读者评论说 ...

  5. 用简单的 Node.js 后台程序浅析 HTTP 请求与响应

    用简单的 Node.js 后台程序浅析 HTTP 请求与响应 本文写于 2020 年 1 月 18 日 我们来看两种方式发送 HTTP 请求,一种呢,是命令行的 curl 命令:一种呢是直接在浏览器的 ...

  6. zabbix-agent python脚本侦听服务器异常登录,并告警

    py脚本 import re,subprocess,time,datetime #gpasswd -a zabbix adm def ftime(a): a = a.replace('Jan','01 ...

  7. OpenStack 安装 Keystone

    OpenStack 安装 Keystone 本篇主要记录一下 如何安装 openstack的 第一个组件 keystone 认证授权组件 openstack 版本 我选的是queens 版本 1.Op ...

  8. js 定时器 Timer

    1 /* Timer 定时器 2 3 parameter: 4 func: Function; //定时器运行时的回调; 默认 null 5 speed: Number; //延迟多少毫秒执行一次 f ...

  9. Flink中如何实现一个自定义MetricReporter

    什么是 Metrics 在 flink 任务运行的过程中,用户通常想知道任务运行的一些基本指标,比如吞吐量.内存和 cpu 使用情况.checkpoint 稳定性等等.而通过 flink metric ...

  10. Typecho博客转移服务器,数据备份.

    目录 Typecho博客转移服务器,数据备份. 简述操作(有基础的mjj看这个简述就可以了.) 详细步骤(建议小白来看, 已经在很多详细方面进行说明了.) 备份篇 备份导入与数据库转移篇 重新部署ty ...