RocketMQ ConsumeQueue与IndexFile实时更新机制源码解析

目录
  • 前言
    • ConsumeQueue详解
    • IndexFile详解
    • IndexHeader
    • slots槽位
    • indexes索引数据
  • 实时更新ConsumeQueue与IndexFile源码分析
    • CommitLogDispatcherBuildConsumeQueue源码分析
    • CommitLogDispatcherBuildIndex源码分析
    • IndexFile如何解决Hash冲突
  • 总结

前言

前面我们介绍了消息是如何存储的,消息是如何刷盘的,讲的都是CommitLog是如何存储和刷盘的。虽然CommitLog顺序存储着所有消息,但是CommitLog中的消息并没有区分topic、keys等,如果需要消费某个topic的消息或者查找某一条消息只能遍历CommitLog文件去查找,性能相当低下,因此有了ConsumeLog和IndexFile两个文件类型,这两个文件的作用主要是提升消息消费和查询的性能。

ConsumeQueue详解

为了提高消费消息查询性能,Broker会为每个Topic在~/store/consumequeue中创建一个Topic名称的目录,并再为该Topic创建目录名为queueId的目录,每个目录存放着若干consumequeue文件,consumequeue属于commitLog的索引文件,可以根据consumequeue定位到具体的消息,consumequeue存储文件见下图

consumequeue文件名由20位数字构成,表示当前文件的第一个索引条目的起始偏移量。与commitLog文件名不同的是,consumequeue后续文件名是固定的,由于consumequeue文件大小是固定不变的。

consumequeue文件大小由mappedFileSizeConsumeQueue配置控制,它的默认大小是30W * ConsumeQueue.CQ_STORE_UNIT_SIZE(20),也就是600W字节大小,ConsumeQueue.CQ_STORE_UNIT_SIZE是consumequeue每个索引条目的大小,每隔索引条目包含了三个消息的重要属性:消息在mappedFile文件中的物理偏移量(8字节)、消息的长度(4字节)、消息Tag的hashcode值,这三个属性占了20个字节,单个索引条目结构如下图所示

IndexFile详解

RocketMQ除了提供消息的Topic给消息消费外,RocketMQ还提供了根据key来查找消息的功能,producer创建消息时可以传入keys值,用于快速查找消息。

// 构建Message参数
Message msg = new Message("TopicTest",  // 消息topic
    "TagA",															// 消息Tag
    "key1 key2 key3",										// 消息keys,多个key用" "隔开
    "hello linshifu!".getBytes(RemotingHelper.DEFAULT_CHARSET)); // 消息体

IndexFile可以看做是一个key的哈希索引文件,通过计算key的hash值,快速找到某个key对应的消息在commitLog中的位置。IndexFile由下面三个部分构成:

  • indexHeader
  • slots槽位
  • indexes索引数据

IndexFile结构如下图所示

每个IndexFile的长度是固定的,其中indexHeader占用40字节,slots占用500W * 4字节,Index索引数据占用2000W * 20字节

IndexHeader

IndexHeader占用IndexFile的前40个字节,它主要存储着IndexFile索引文件的相关信息,IndexHeader包含如下属性

// org.apache.rocketmq.store.index.IndexHeader
public class IndexHeader {
    // 索引文件第一条消息在commitLog中的存储时间
    private final AtomicLong beginTimestamp = new AtomicLong(0);
    // 索引文件最后一条消息在commitLog中的存储时间
    private final AtomicLong endTimestamp = new AtomicLong(0);
    // 索引文件第一条消息的偏移量
    private final AtomicLong beginPhyOffset = new AtomicLong(0);
    // 索引文件最后一条消息的偏移量
    private final AtomicLong endPhyOffset = new AtomicLong(0);
    // 已经填充slot的hash槽数量
    private final AtomicInteger hashSlotCount = new AtomicInteger(0);
    // 该indexFile种包含的索引单元数量
    private final AtomicInteger indexCount = new AtomicInteger(1);
}

数据结构如下图所示

slots槽位

在IndexFile中间部分存储的是IndexFlie中key的hash槽,每个hash槽存储的是index索引单元的indexNo,添加索引时会将key的hash值%500W的结果计算哈希槽序号,然后将index索引单元的indexNo放入slot槽中,indexNo是int类型,slots槽位总共有500W个,因此slots槽位占用的大小是500w * 4=2000w

indexes索引数据

index索引由2000W个索引单元构成,每个索引单元大小为20字节,每隔索引单元由下面四个部分构成

  • keyHash

keyHash是消息索引key的Hash值

  • phyOffet

phyOffset是当前key对应消息在commitLog中的偏移量commitLog offset

  • timeDiff

timeDiff是当前key对应消息存储时间与当前indexFile第一个索引存储时间差

  • preIndex

当前slot的index索引单元的前一个索引单元的indexNo

索引单元数据结构如下

实时更新ConsumeQueue与IndexFile源码分析

之前的文章我们只了解了Broker的CommitLog文件保存和刷盘的流程,现在我们来了解Broker实时更新ConsumeQueue和IndexFile的流程。

消息保存的过程仅仅会保存CommitLog,ConsumeQueue文件及IndexFile中的数据是通过ReputMessageService将CommitLog中的消息转发到ConsumeQueue及IndexFile。

ReputMessageService和之前的刷盘服务类似,都是异步线程执行的服务。ReputMessageService是DefaultMessageStore的一个内部类,它跟随者消息存储对象DefaultMessageStore创建时共同创建。ReputMessageService刷新ConsumeQueue与IndexFile的逻辑可以从它的run()方法开始分析。

// org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService#run
@Override
public void run() {
		// 死循环
    while (!this.isStopped()) {
        try {
             // 睡眠1ms
            Thread.sleep(1);
						// 更新consumeQueue和IndexFile
            this.doReput();
        } catch (Exception e) {
            DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }
}

从上面代码可以看出,更新ConsumeQueue与IndexFile在死循环中执行,每隔1ms执行一次doReput()来更新更新consumeQueue和IndexFile,在doReput()中的主要逻辑如下

  • 如果重放消息偏移量reputFromOffset小于CommitLog的最大offset,则会循环重放消息,更新ConsumeQueue及IndexFile
  • 从CommitLog的重放偏移量开始获取映射缓冲结果SelectMappedBufferResult,SelectMappedBufferResult包含如下属性
// org.apache.rocketmq.store.SelectMappedBufferResult
public class SelectMappedBufferResult {
    // mappedFile文件起始偏移量+position
    private final long startOffset;
    // reputFromOffset开始的缓冲
    private final ByteBuffer byteBuffer;
    // 消息size
    private int size;
    // commitLog的MappedFile
    private MappedFile mappedFile;
}
  • 根据SelectMappedBufferResult校验消息,并创建转发请求DispatchRequest,DispatchRequest中包含更新ConsumeQueue和IndexFile中需要用到的属性,如topic,消息偏移量,消息key,消息存储时间戳,消息长度,消息tagHashCode等。
  • 如果当前消息size>0,则说明当前消息需要被转发更新ConsumeQueue和IndexFile,会调用关键方法DefaultMessageStore.this.doDispatch转发更新
  • 如果当前消息size=0,则说明已经读到了CommitLog当前MappedFile的结尾,因此需要读取下一个MappedFile,并进行转发。
// org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService#doReput
private void doReput() {
    // 1.reputFromOffset ≤ commitLog最大offset,则循环重放
    for (boolean doNext = true; this.isCommitLogAvailable()/*reputFromOffset≤commitLog最大offset*/&&doNext; ) {
        // 2.根据reputFromOffset的物理偏移量找到mappedFileQueue中对应的CommitLog文件的MappedFile
        // 然后从该MappedFile中截取一段自reputFromOffset偏移量开始的ByteBuffer,这段内存存储着将要重放的消息
        SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
        if (result != null) {
            try {
                // 遍历消息,开始reput
                for (int readSize = 0; readSize < result.getSize() && doNext; ) {
                    // 3. 检查消息属性,并构建一个消息的dispatchRequest
                    DispatchRequest dispatchRequest =
                        DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
                    if (dispatchRequest.isSuccess()) {
                        if (size > 0) {
                            // 4.消息分发,写consumeQueue和Index
                            DefaultMessageStore.this.doDispatch(dispatchRequest);
                            // 设置reputOffset加上当前消息大小
                            this.reputFromOffset += size;
                            // 设置读取的大小加上当前消息大小
                            readSize += size;
                             //如果size=0,说明读取到了MappedFile的文件结尾
                        } else if (size == 0) {
                            // 5. 获取下个文件的起始offset
                            this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
                            // 设置readSize=0,结束循环
                            readSize = result.getSize();
                        }
                    } else if (!dispatchRequest.isSuccess()) {
                       // ...
                }
            } finally {
                result.release();
            }
        } else {
            doNext = false;
        }
    }
}

由上面代码可知,转发更新ConsumeQueue和IndexFile的关键代码在DefaultMessageStore.this.doDispatch(dispatchRequest)中,在doDispatch()方法中循环遍历dispatcherList中的CommitLogDispatcher。

public void doDispatch(DispatchRequest req) {
    for (CommitLogDispatcher dispatcher : this.dispatcherList) {
        dispatcher.dispatch(req);
    }
}

debug代码可以中包含处理转发请求的Dispatcher类,通过类名就可以很容易判断出CommitLogDispatcherBuildConsumeQueue是将CommitLog转发到ConsumeQueue中,CommitLogDispatcherBuildIndex是将消息构建IndexFile,下面我们来分别分析两者是如何处理CommitLog消息转发的。

CommitLogDispatcherBuildConsumeQueue源码分析

CommitLogDispatcherBuildConsumeQueue将消息保存到ConsumeQueue如下所示,主要是下面两步

  • 先根据消息Topic和QueueId从consumeQueueTable找到ConsumeQueue,如果找不到会创建一个新的consumeQueue
  • 调用ConsumeQueue#putMessagePositionInfoWrapper,将消息保存到consumeQueue中
// org.apache.rocketmq.store.DefaultMessageStore#putMessagePositionInfo
public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
    // 找到ConsumeQueue,如果找不到会创建一个ConsumeQueue
    ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
    // 消息保存到consumeQueue中
    cq.putMessagePositionInfoWrapper(dispatchRequest, checkMultiDispatchQueue(dispatchRequest));
}

保存consumeQueue存储单元消息如下,主要分为下面三个步骤

  • 将consumeQueue存储单元offset(8字节)+消息长度(4字节)+tags的哈希码(8字节)保存到consumeQueue的缓存byteBufferIndex中
  • 根据consumeQueue的offset找到MappedFile
  • 将缓冲中的存储单元存储到MappedFile中
// org.apache.rocketmq.store.ConsumeQueue#putMessagePositionInfo
private boolean putMessagePositionInfo(final long offset, final int size, final long tagsCode,
        final long cqOffset) {
    this.byteBufferIndex.flip();
    // consumeQueue存储单元的长度
    this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
    // 消息物理偏移量
    this.byteBufferIndex.putLong(offset);
    // 消息长度
    this.byteBufferIndex.putInt(size);
    // 消息tags的哈希码
    this.byteBufferIndex.putLong(tagsCode);
    final long expectLogicOffset = cqOffset * CQ_STORE_UNIT_SIZE;
		// 获取最后一个mappedFile
    MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(expectLogicOffset);
    if (mappedFile != null) {
        // 更新物理offset
        this.maxPhysicOffset = offset + size;
      	// 数据保存到consumeQueue
        return mappedFile.appendMessage(this.byteBufferIndex.array());
    }
    return false;
}

CommitLogDispatcherBuildIndex源码分析

除了CommitLogDispatcherBuildConsumeQueue,下面我们来分析在dispatcherList中另一个CommitLogDispatcher的实现类CommitLogDispatcherBuildIndex是如何将Index索引单元保存到IndexFile中的,存储消息索引的核心逻辑如下所示。

  • 获取或者创建最新的IndexFile
  • 将msgId构建Index索引单元并保存到IndexFile中
  • 将Message中的keys用空格分隔成key数组,并循环保存到indexFile中
public void buildIndex(DispatchRequest req) {
    // 获取或者创建最新索引文件,支持重试最多3次
    IndexFile indexFile = retryGetAndCreateIndexFile();
    if (indexFile != null) {
        // 获取结束物理索引
        long endPhyOffset = indexFile.getEndPhyOffset();
        DispatchRequest msg = req;
        // 获取topic和keys
        String topic = msg.getTopic();
        String keys = msg.getKeys();
        // 如果当前消息的commitLogOffset小于当前IndexFile的endPhyOffset时,说明当前消息已经构建过Index索引,因此直接返回
        if (msg.getCommitLogOffset() < endPhyOffset) {
            return;
        }
        // 获取客户端生成的uniqueId(msgId),代表客户端生成的唯一一条消息
        // 消息解密时生成的
        if (req.getUniqKey() != null) {
            indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
        }
        // 客户端传递的keys,消息是从keys属性中获取的
        if (keys != null && keys.length() > 0) {
            String[] keyset = keys.split(MessageConst.KEY_SEPARATOR/*空格*/);
            for (int i = 0; i < keyset.length; i++) {
                String key = keyset[i];
                if (key.length() > 0) {
                    indexFile = putKey(indexFile, msg, buildKey(topic, key));
                    if (indexFile == null) {
                        return;
                    }
                }
            }
        }
    } else {
        log.error("build index error, stop building index");
    }
}

从上面源码可知,保存消息的关键就在putKey方法中主要分为下面三个步骤

  • 获取要保存到IndexFile的keyHashCode(keyHash),hashSlot的绝对位置(absSlotPos),hash槽中的索引值(slotValue),保存消息时间差(timeDiff),索引的绝对位置(absIndexPos)等。
  • 更新Index索引单元信息,keyHashCode(keyHash),消息在commitLog中的偏移量(phyOffset),消息存储时间与索引文件开始存储时间差(timeDiff),前置消息索引值(slotValue)
  • 更新slots的IndexCount
  • 更新IndexHeader中的indexCount,更新物理偏移量(phyoffset),最后存储时间戳(sotreTimestamp)
    public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
        // 索引数量小于2000W,否则说明当前索引文件已经满了,不能添加索引
        if (this.indexHeader.getIndexCount() < this.indexNum) {
            // keyHashCode
            int keyHash = indexKeyHashMethod(key);
            // 索引槽位置
            int slotPos = keyHash % this.hashSlotNum;
            // 绝对位置
            int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
            try {
                int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
                long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();
                int absIndexPos =
                    IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize/*哈希槽数量*哈希槽大小=500w*4*/
                        + this.indexHeader.getIndexCount() * indexSize;
                // 更新IndexFile索引单元信息
              	// keyHash(4)+消息在commitLog中的偏移量(8)+消息存储时间-索引文件开始存储时间(4)+前置消息索引值(4)
                this.mappedByteBuffer.putInt(absIndexPos/*索引位置*/, keyHash);
                this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
                this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
                this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);
								// 更新slots的indexCount
                this.mappedByteBuffer.putInt(absSlotPos/*hash槽的绝对位置*/, this.indexHeader.getIndexCount());
              	//...
                // 更新IndexHeader信息
                this.indexHeader.incIndexCount();
                this.indexHeader.setEndPhyOffset(phyOffset);
                this.indexHeader.setEndTimestamp(storeTimestamp);
                return true;
            } catch (Exception e) {
                log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
            }
        }
        return false;
    }

IndexFile如何解决Hash冲突

假设在IndexFile的索引IndexN的是一个keyHash为100的索引,如下图所示,此时slots槽位100存储着indexN的序号,在IndexFile索引单元保存的数据keyHash=100,preIndexNo=0。

如果又有一个索引单元indexN+X的keyHashCode=100,保存消息时发现solt-100已经指向了索引单元indexN,会将当前索引单元IndxeN+X的preIndexNo更新为indexN,使得当前索引单元indexN+X的前置索引单元指向indeNo,再更新slots-100槽位的值为indexN+X,保存完成后的索引关系如下图所示。相当于在slots槽位下面挂了index索引单元链表,根据key查找消息时,可以根据key计算出keyHashCode,然后顺着链表查询链表中的消息。

总结

ConsumeQueue可以看成是消息消费的索引,不同Topic的ConsumeQueue存储到不同目录中,默认存储在~/store/consumequeue/${topic}目录中,其底层也是使用MappedFile,Broker会按照消息在CommitLog中的顺序,异步转发到ConsumeQueue中,每条消息在ConsumeQueue生成固定大小20字节的存储单元指向CommitLog。

IndexFile保存着Producer发送消息keys中的索引,有了IndexFile就可以根据消息key快速找到消息。IndexFile的数据接口与HashMap类似,它使用链表的方式解决解决哈希冲突,并且使用头插法将数据插入链表中。

以上就是RocketMQ ConsumeQueue与IndexFile实时更新机制源码解析的详细内容,更多关于RocketMQ 实时更新机制的资料请关注我们其它相关文章!

(0)

相关推荐

  • RocketMQ producer同步发送单向发送源码解析

    目录 RocketMQ生产者发送消息分为三种模式 1. 同步发送 1.1 DefaultMQProducerImpl#sendDefaultImpl 1.2 DefaultMQProducerImpl#sendKernelImpl 1.3 MQClientAPIImpl#sendMessage 1.4 MQClientAPIImpl#sendMessageSync 1.5 NettyRemotingClient#invokeSync 2. 单向发送 2.1 DefaultMQProducerIm

  • RocketMQ producer容错机制源码解析

    目录 1. 前言 2. 失败重试 3. 延迟故障 3.1 最普通的选择策略 3.2 延迟故障的实现 1. 前言 本文主要是介绍一下RocketMQ消息生产者在发送消息的时候发送失败的问题处理?这里有两个点,一个是关于消息的处理,一个是关于broker的处理,比如说发送消息到broker-a的broker失败了,我们可能下次就不想发送到这个broker-a,这就涉及到一个选择broker的问题,也就是选择MessageQueue的问题. 2. 失败重试 其实失败重试我们在介绍RocketMQ消息生

  • RocketMQ broker文件清理源码解析

    目录 1. broker 清理文件介绍 1.1 哪些文件需要清理 1.2 RocketMQ文件清理的机制 2. 源码解析 2.1 清理commitlog 2.2 ConsumeQueue 清理 2.3 indexFile 清理 3. 总结 1. broker 清理文件介绍 本系列RocketMQ4.8注释github地址,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈 1.1 哪些文件需要清理 首先我们需要介绍下在RocketMQ中哪些文件需要清理,其实可以想一想,在RocketMQ

  • RocketMQ broker 消息投递流程处理PULL_MESSAGE请求解析

    目录 RocketMq消息处理 1. 处理PULL_MESSAGE请求 2. 获取消息 3. 挂起请求:PullRequestHoldService#suspendPullRequest 3.1 处理挂起请求的线程:PullRequestHoldService 3.2 唤醒请求:PullMessageProcessor#executeRequestWhenWakeup 3.3 消息分发中唤醒consumer请求 总结 RocketMq消息处理 RocketMq消息处理整个流程如下: 本系列Roc

  • 详解Redis 缓存删除机制(源码解析)

    删除的范围 过期的 key 在内存满了的情况下,如果继续执行 set 等命令,且所有 key 都没有过期,那么会按照缓存淘汰策略选中的 key 过期删除 redis 中设置了过期时间的 key 会单独存储一份 typedef struct redisDb { dict *dict; // 所有的键值对 dict *expires; //设置了过期时间的键值对 // ... } redisDb; 设置有效期 Redis 中有 4 个命令可以给 key 设置过期时间,分别是 expire pexpi

  • Kubernetes controller manager运行机制源码解析

    目录 Run StartControllers ReplicaSet ReplicaSetController syncReplicaSet Summary Run 确立目标 理解 kube-controller-manager 的运行机制 从主函数找到run函数,代码较长,这里精简了一下 func Run(c *config.CompletedConfig, stopCh <-chan struct{}) error { // configz 模块,在kube-scheduler分析中已经了解

  • React事件机制源码解析

    React v17里事件机制有了比较大的改动,想来和v16差别还是比较大的. 本文浅析的React版本为17.0.1,使用ReactDOM.render创建应用,不含优先级相关. 原理简述 React中事件分为委托事件(DelegatedEvent)和不需要委托事件(NonDelegatedEvent),委托事件在fiberRoot创建的时候,就会在root节点的DOM元素上绑定几乎所有事件的处理函数,而不需要委托事件只会将处理函数绑定在DOM元素本身. 同时,React将事件分为3种类型--d

  • Spring AOP实现声明式事务机制源码解析

    目录 一.声明式全局事务 二.源码 三.小结: 一.声明式全局事务 在Seata示例工程中,能看到@GlobalTransactional,如下方法示例: @GlobalTransactional public boolean purchase(long accountId, long stockId, long quantity) { String xid = RootContext.getXID(); LOGGER.info("New Transaction Begins: " +

  • 浅谈Vuejs中nextTick()异步更新队列源码解析

    vue官网关于此解释说明如下: vue2.0里面的深入响应式原理的异步更新队列 官网说明如下: 只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变.如果同一个 watcher 被多次触发,只会一次推入到队列中.这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要.然后,在下一个的事件循环"tick"中,Vue 刷新队列并执行实际(已去重的)工作.Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MutationOb

  • jquery事件绑定解绑机制源码解析

    引子 为什么Jquery能实现不传回调函数也能解绑事件?如下: $("p").on("click",function(){ alert("The paragraph was clicked."); }); $("#box1").off("click"); 事件绑定解绑机制 调用on函数的时候,将生成一份事件数据,结构如下: { type: type, origType: origType, data: da

  • RocketMQ源码解析topic创建机制详解

    目录 1. RocketMQ Topic创建机制 2. 自动Topic 3. 手动创建--预先创建 通过界面控制台创建 1. RocketMQ Topic创建机制 以下源码基于Rocket MQ 4.7.0 RocketMQ Topic创建机制分为两种:一种自动创建,一种手动创建.可以通过设置broker的配置文件来禁用或者允许自动创建.默认是开启的允许自动创建 autoCreateTopicEnable=true/false 下面会结合源码来深度分析一下自动创建和手动创建的过程. 2. 自动T

  • Nacos客户端配置中心缓存动态更新实现源码

    目录 客户端配置缓存更新 长轮训任务启动入口 ClientWorker checkConfigInfo LongPollingRunnable.run checkLocalConfig checkListenerMd5 检查服务端配置 checkUpdateDataIds checkUpdateConfigStr 客户端缓存配置长轮训机制总结 服务端配置更新的推送 doPollingConfig addLongPollingClient ClientLongPolling allSubs Lon

  • Android消息循环机制源码深入理解

    Android消息循环机制源码 前言: 搞Android的不懂Handler消息循环机制,都不好意思说自己是Android工程师.面试的时候一般也都会问这个知识点,但是我相信大多数码农肯定是没有看过相关源码的,顶多也就是网上搜搜,看看别人的文章介绍.学姐不想把那个万能的关系图拿出来讨论. 近来找了一些关于android线程间通信的资料,整理学习了一下,并制作了一个简单的例子. andriod提供了 Handler 和 Looper 来满足线程间的通信.例如一个子线程从网络上下载了一副图片,当它下

  • linux下SVN配置实现项目目录自动更新以及源码安装的操作方法

    配置钩子文件自动更新 开发环境提交更新至服务器时会出现每次在服务器端项目目录下必须手动更新SVN up才可以访问最新更新,通过钩子文件配置 则可以实现自动更新 新建文件: vim /usr/local/svn/demo/hooks/post-commit 添加如下文字: #!/bin/sh export LANG=en_US.UTF-8 /usr/bin/svn update /var/www/html/demo --username dev1 --password 123456 再添加post

随机推荐