源码分析 RocketMQ DLedger(多副本) 之日志复制(传播)

接下来我们将详细介绍上述4个类,从而揭晓日志复制的核心实现原理。

1、DLedgerEntryPusher


1.1 核心类图

在这里插入图片描述

DLedger 多副本日志推送的核心实现类,里面会创建 EntryDispatcher、QuorumAckChecker、EntryHandler 三个核心线程。其核心属性如下:

  • DLedgerConfig dLedgerConfig

多副本相关配置。

  • DLedgerStore dLedgerStore

存储实现类。

  • MemberState memberState

节点状态机。

  • DLedgerRpcService dLedgerRpcService

RPC 服务实现类,用于集群内的其他节点进行网络通讯。

  • Map<Long, ConcurrentMap<String, Long>> peerWaterMarksByTerm

每个节点基于投票轮次的当前水位线标记。键值为投票轮次,值为 ConcurrentMap<String/** 节点id*/, Long/** 节点对应的日志序号*/>。

  • Map<Long, ConcurrentMap<Long, TimeoutFuture>> pendingAppendResponsesByTerm

用于存放追加请求的响应结果(Future模式)。

  • EntryHandler entryHandler

从节点上开启的线程,用于接收主节点的 push 请求(append、commit、append)。

  • QuorumAckChecker quorumAckChecker

主节点上的追加请求投票器。

  • Map<String, EntryDispatcher> dispatcherMap

主节点日志请求转发器,向从节点复制消息等。

接下来介绍一下其核心方法的实现。

1.2 构造方法

public DLedgerEntryPusher(DLedgerConfig dLedgerConfig, MemberState memberState, DLedgerStore dLedgerStore,

DLedgerRpcService dLedgerRpcService) {

this.dLedgerConfig = dLedgerConfig;

this.memberState = memberState;

this.dLedgerStore = dLedgerStore;

this.dLedgerRpcService = dLedgerRpcService;

for (String peer : memberState.getPeerMap().keySet()) {

if (!peer.equals(memberState.getSelfId())) {

dispatcherMap.put(peer, new EntryDispatcher(peer, logger));

}

}

}

构造方法的重点是会根据集群内的节点,依次构建对应的 EntryDispatcher 对象。

1.3 startup

DLedgerEntryPusher#startup

public void startup() {

entryHandler.start();

quorumAckChecker.start();

for (EntryDispatcher dispatcher : dispatcherMap.values()) {

dispatcher.start();

}

}

依次启动 EntryHandler、QuorumAckChecker 与 EntryDispatcher 线程。

备注:DLedgerEntryPusher 的其他核心方法在详细分析其日志复制原理的过程中会一一介绍。

接下来将从 EntryDispatcher、QuorumAckChecker、EntryHandler 来阐述 RocketMQ DLedger(多副本)的实现原理。

2、EntryDispatcher 详解


2.1 核心类图

在这里插入图片描述

其核心属性如下。

  • AtomicReference<PushEntryRequest.Type> type = new AtomicReference<>(PushEntryRequest.Type.COMPARE)

向从节点发送命令的类型,可选值:PushEntryRequest.Type.COMPARE、TRUNCATE、APPEND、COMMIT,下面详细说明。

  • long lastPushCommitTimeMs = -1

上一次发送提交类型的时间戳。

  • String peerId

目标节点ID。

  • long compareIndex = -1

已完成比较的日志序号。

  • long writeIndex = -1

已写入的日志序号。

  • int maxPendingSize = 1000

允许的最大挂起日志数量。

  • long term = -1

Leader 节点当前的投票轮次。

  • String leaderId = null

Leader 节点ID。

  • long lastCheckLeakTimeMs = System.currentTimeMillis()

上次检测泄漏的时间,所谓的泄漏,就是看挂起的日志请求数量是否查过了 maxPendingSize 。

  • ConcurrentMap<Long, Long> pendingMap = new ConcurrentHashMap<>()

记录日志的挂起时间,key:日志的序列(entryIndex),value:挂起时间戳。

  • Quota quota = new Quota(dLedgerConfig.getPeerPushQuota())

配额。

2.2 Push 请求类型

DLedger 主节点向从从节点复制日志总共定义了4类请求类型,其枚举类型为 PushEntryRequest.Type,其值分别为 COMPARE、TRUNCATE、APPEND、COMMIT。

  • COMPARE

如果 Leader 发生变化,新的 Leader 需要与他的从节点的日志条目进行比较,以便截断从节点多余的数据。

  • TRUNCATE

如果 Leader 通过索引完成日志对比,则 Leader 将发送 TRUNCATE 给它的从节点。

  • APPEND

将日志条目追加到从节点。

  • COMMIT

通常,leader 会将提交的索引附加到 append 请求,但是如果 append 请求很少且分散,leader 将发送一个单独的请求来通知从节点提交的索引。

对主从节点的请求类型有了一个初步的认识后,我们将从 EntryDispatcher 的业务处理入口 doWork 方法开始讲解。

2.3 doWork 方法详解

public void doWork() {

try {

if (!checkAndFreshState()) { // @1

waitForRunning(1);

return;

}

if (type.get() == PushEntryRequest.Type.APPEND) { // @2

doAppend();

} else {

doCompare(); // @3

}

waitForRunning(1);

} catch (Throwable t) {

DLedgerEntryPusher.logger.error(“[Push-{}]Error in {} writeIndex={} compareIndex={}”, peerId, getName(), writeIndex, compareIndex, t);

DLedgerUtils.sleep(500);

}

}

代码@1:检查状态,是否可以继续发送 append 或 compare。

代码@2:如果推送类型为APPEND,主节点向从节点传播消息请求。

代码@3:主节点向从节点发送对比数据差异请求(当一个新节点被选举成为主节点时,往往这是第一步)。

2.3.1 checkAndFreshState 详解

EntryDispatcher#checkAndFreshState

private boolean checkAndFreshState() {

if (!memberState.isLeader()) { // @1

return false;

}

if (term != memberState.currTerm() || leaderId == null || !leaderId.equals(memberState.getLeaderId())) { // @2

synchronized (memberState) {

if (!memberState.isLeader()) {

return false;

}

PreConditions.check(memberState.getSelfId().equals(memberState.getLeaderId()), DLedgerResponseCode.UNKNOWN);

term = memberState.currTerm();

leaderId = memberState.getSelfId();

changeState(-1, PushEntryRequest.Type.COMPARE);

}

}

return true;

}

代码@1:如果节点的状态不是主节点,则直接返回 false。则结束 本次 doWork 方法。因为只有主节点才需要向从节点转发日志。

代码@2:如果当前节点状态是主节点,但当前的投票轮次与状态机轮次或 leaderId 还未设置,或 leaderId 与状态机的 leaderId 不相等,这种情况通常是集群触发了重新选举,设置其term、leaderId与状态机同步,即将发送COMPARE 请求。

接下来看一下 changeState (改变状态)。

private synchronized void changeState(long index, PushEntryRequest.Type target) {

logger.info(“[Push-{}]Change state from {} to {} at {}”, peerId, type.get(), target, index);

switch (target) {

case APPEND: // @1

compareIndex = -1;

updatePeerWaterMark(term, peerId, index);

quorumAckChecker.wakeup();

writeIndex = index + 1;

break;

case COMPARE: // @2

if (this.type.compareAndSet(PushEntryRequest.Type.APPEND, PushEntryRequest.Type.COMPARE)) {

compareIndex = -1;

pendingMap.clear();

}

break;

case TRUNCATE: // @3

compareIndex = -1;

break;

default:

break;

}

type.set(target);

}

代码@1:如果将目标类型设置为 append,则重置 compareIndex ,并设置 writeIndex 为当前 index 加1。

代码@2:如果将目标类型设置为 COMPARE,则重置 compareIndex 为负一,接下将向各个从节点发送 COMPARE 请求类似,并清除已挂起的请求。

代码@3:如果将目标类型设置为 TRUNCATE,则重置 compareIndex 为负一。

接下来具体来看一下 APPEND、COMPARE、TRUNCATE 等请求。

2.3.2 append 请求详解

EntryDispatcher#doAppend

private void doAppend() throws Exception {

while (true) {

if (!checkAndFreshState()) { // @1

break;

}

if (type.get() != PushEntryRequest.Type.APPEND) { // @2

break;

}

if (writeIndex > dLedgerStore.getLedgerEndIndex()) { // @3

doCommit();

doCheckAppendResponse();

break;

}

if (pendingMap.size() >= maxPendingSize || (DLedgerUtils.elapsed(lastCheckLeakTimeMs) > 1000)) { // @4

long peerWaterMark = getPeerWaterMark(term, peerId);

for (Long index : pendingMap.keySet()) {

if (index < peerWaterMark) {

pendingMap.remove(index);

}

}

lastCheckLeakTimeMs = System.currentTimeMillis();

}

if (pendingMap.size() >= maxPendingSize) { // @5

doCheckAppendResponse();

break;

}

doAppendInner(writeIndex); // @6

writeIndex++;

}

}

代码@1:检查状态,已经在上面详细介绍。

代码@2:如果请求类型不为 APPEND,则退出,结束本轮 doWork 方法执行。

代码@3:writeIndex 表示当前追加到从该节点的序号,通常情况下主节点向从节点发送 append 请求时,会附带主节点的已提交指针,但如何 append 请求发不那么频繁,writeIndex 大于 leaderEndIndex 时(由于pending请求超过其 pending 请求的队列长度(默认为1w),时,会阻止数据的追加,此时有可能出现 writeIndex 大于 leaderEndIndex 的情况,此时单独发送 COMMIT 请求。

代码@4:检测 pendingMap(挂起的请求数量)是否发送泄漏,即挂起队列中容量是否超过允许的最大挂起阀值。获取当前节点关于本轮次的当前水位线(已成功 append 请求的日志序号),如果发现正在挂起请求的日志序号小于水位线,则丢弃。

代码@5:如果挂起的请求(等待从节点追加结果)大于 maxPendingSize 时,检查并追加一次 append 请求。

代码@6:具体的追加请求。

2.3.2.1 doCommit 发送提交请求

EntryDispatcher#doCommit

private void doCommit() throws Exception {

if (DLedgerUtils.elapsed(lastPushCommitTimeMs) > 1000) { // @1

PushEntryRequest request = buildPushRequest(null, PushEntryRequest.Type.COMMIT); // @2

//Ignore the results

dLedgerRpcService.push(request); // @3

lastPushCommitTimeMs = System.currentTimeMillis();

}

}

代码@1:如果上一次单独发送 commit 的请求时间与当前时间相隔低于 1s,放弃本次提交请求。

代码@2:构建提交请求。

代码@3:通过网络向从节点发送 commit 请求。

接下来先了解一下如何构建 commit 请求包。

EntryDispatcher#buildPushRequest

private PushEntryRequest buildPushRequest(DLedgerEntry entry, PushEntryRequest.Type target) {

PushEntryRequest request = new PushEntryRequest();

request.setGroup(memberState.getGroup());

request.setRemoteId(peerId);

request.setLeaderId(leaderId);

request.setTerm(term);

request.setEntry(entry);

request.setType(target);

request.setCommitIndex(dLedgerStore.getCommittedIndex());

return request;

}

提交包请求字段主要包含如下字段:DLedger 节点所属组、从节点 id、主节点 id,当前投票轮次、日志内容、请求类型与 committedIndex(主节点已提交日志序号)。

2.3.2.2 doCheckAppendResponse 检查并追加请求

EntryDispatcher#doCheckAppendResponse

private void doCheckAppendResponse() throws Exception {

long peerWaterMark = getPeerWaterMark(term, peerId); // @1

Long sendTimeMs = pendingMap.get(peerWaterMark + 1);

if (sendTimeMs != null && System.currentTimeMillis() - sendTimeMs > dLedgerConfig.getMaxPushTimeOutMs()) { // @2

logger.warn(“[Push-{}]Retry to push entry at {}”, peerId, peerWaterMark + 1);

doAppendInner(peerWaterMark + 1);

}

}

该方法的作用是检查 append 请求是否超时,其关键实现如下:

  • 获取已成功 append 的序号。

  • 从挂起的请求队列中获取下一条的发送时间,如果不为空并去超过了 append 的超时时间,则再重新发送 append 请求,最大超时时间默认为 1s,可以通过 maxPushTimeOutMs 来改变默认值。

2.3.2.3 doAppendInner 追加请求

向从节点发送 append 请求。

EntryDispatcher#doAppendInner

private void doAppendInner(long index) throws Exception {

DLedgerEntry entry = dLedgerStore.get(index); // @1

PreConditions.check(entry != null, DLedgerResponseCode.UNKNOWN, “writeIndex=%d”, index);

checkQuotaAndWait(entry); // @2

PushEntryRequest request = buildPushRequest(entry, PushEntryRequest.Type.APPEND); // @3

CompletableFuture responseFuture = dLedgerRpcService.push(request); // @4

pendingMap.put(index, System.currentTimeMillis()); // @5

responseFuture.whenComplete((x, ex) -> {

try {

PreConditions.check(ex == null, DLedgerResponseCode.UNKNOWN);

DLedgerResponseCode responseCode = DLedgerResponseCode.valueOf(x.getCode());

switch (responseCode) {

case SUCCESS: // @6

pendingMap.remove(x.getIndex());

updatePeerWaterMark(x.getTerm(), peerId, x.getIndex());

quorumAckChecker.wakeup();

break;

case INCONSISTENT_STATE: // @7

logger.info(“[Push-{}]Get INCONSISTENT_STATE when push index={} term={}”, peerId, x.getIndex(), x.getTerm());

changeState(-1, PushEntryRequest.Type.COMPARE);

break;

default:

logger.warn(“[Push-{}]Get error response code {} {}”, peerId, responseCode, x.baseInfo());

break;

}

} catch (Throwable t) {

logger.error(“”, t);

}

});

lastPushCommitTimeMs = System.currentTimeMillis();

}

代码@1:首先根据序号查询出日志。

代码@2:检测配额,如果超过配额,会进行一定的限流,其关键实现点:

  • 首先触发条件:append 挂起请求数已超过最大允许挂起数;基于文件存储并主从差异超过300m,可通过 peerPushThrottlePoint 配置。

  • 每秒追加的日志超过 20m(可通过 peerPushQuota 配置),则会 sleep 1s中后再追加。

代码@3:构建 PUSH 请求日志。

代码@4:通过 Netty 发送网络请求到从节点,从节点收到请求会进行处理(本文并不会探讨与网络相关的实现细节)。

代码@5:用 pendingMap 记录待追加的日志的发送时间,用于发送端判断是否超时的一个依据。

代码@6:请求成功的处理逻辑,其关键实现点如下:

  • 移除 pendingMap 中的关于该日志的发送超时时间。

  • 更新已成功追加的日志序号(按投票轮次组织,并且每个从服务器一个键值对)。

  • 唤醒 quorumAckChecker 线程(主要用于仲裁 append 结果),后续会详细介绍。

代码@7:Push 请求出现状态不一致情况,将发送 COMPARE 请求,来对比主从节点的数据是否一致。

日志转发 append 追加请求类型就介绍到这里了,接下来我们继续探讨另一个请求类型 compare。

2.3.3 compare 请求详解

COMPARE 类型的请求有 doCompare 方法发送,首先该方法运行在 while (true) 中,故在查阅下面代码时,要注意其退出循环的条件。

EntryDispatcher#doCompare

if (!checkAndFreshState()) {

break;

}

if (type.get() != PushEntryRequest.Type.COMPARE

&& type.get() != PushEntryRequest.Type.TRUNCATE) {

break;

}

if (compareIndex == -1 && dLedgerStore.getLedgerEndIndex() == -1) {

break;

}

Step1:验证是否执行,有几个关键点如下:

  • 判断是否是主节点,如果不是主节点,则直接跳出。

  • 如果是请求类型不是 COMPARE 或 TRUNCATE 请求,则直接跳出。

  • 如果已比较索引 和 ledgerEndIndex 都为 -1 ,表示一个新的 DLedger 集群,则直接跳出。

EntryDispatcher#doCompare

if (compareIndex == -1) {

compareIndex = dLedgerStore.getLedgerEndIndex();

logger.info(“[Push-{}][DoCompare] compareIndex=-1 means start to compare”, peerId);

} else if (compareIndex > dLedgerStore.getLedgerEndIndex() || compareIndex < dLedgerStore.getLedgerBeginIndex()) {

logger.info(“[Push-{}][DoCompare] compareIndex={} out of range {}-{}”, peerId, compareIndex, dLedgerStore.getLedgerBeginIndex(), dLedgerStore.getLedgerEndIndex());

compareIndex = dLedgerStore.getLedgerEndIndex();

}

Step2:如果 compareIndex 为 -1 或compareIndex 不在有效范围内,则重置待比较序列号为当前已已存储的最大日志序号:ledgerEndIndex。

DLedgerEntry entry = dLedgerStore.get(compareIndex);

PreConditions.check(entry != null, DLedgerResponseCode.INTERNAL_ERROR, “compareIndex=%d”, compareIndex);

PushEntryRequest request = buildPushRequest(entry, PushEntryRequest.Type.COMPARE);

CompletableFuture responseFuture = dLedgerRpcService.push(request);

PushEntryResponse response = responseFuture.get(3, TimeUnit.SECONDS);

Step3:根据序号查询到日志,并向从节点发起 COMPARE 请求,其超时时间为 3s。

EntryDispatcher#doCompare

long truncateIndex = -1;

if (response.getCode() == DLedgerResponseCode.SUCCESS.getCode()) { // @1

if (compareIndex == response.getEndIndex()) {

changeState(compareIndex, PushEntryRequest.Type.APPEND);

break;

} else {

truncateIndex = compareIndex;

}

} else if (response.getEndIndex() < dLedgerStore.getLedgerBeginIndex()

|| response.getBeginIndex() > dLedgerStore.getLedgerEndIndex()) { // @2

truncateIndex = dLedgerStore.getLedgerBeginIndex();

} else if (compareIndex < response.getBeginIndex()) { // @3

truncateIndex = dLedgerStore.getLedgerBeginIndex();

} else if (compareIndex > response.getEndIndex()) { // @4

compareIndex = response.getEndIndex();

} else { // @5

compareIndex–;

}

if (compareIndex < dLedgerStore.getLedgerBeginIndex()) { // @6

truncateIndex = dLedgerStore.getLedgerBeginIndex();

}

Step4:根据响应结果计算需要截断的日志序号,其主要实现关键点如下:

  • 代码@1:如果两者的日志序号相同,则无需截断,下次将直接先从节点发送 append 请求;否则将 truncateIndex 设置为响应结果中的 endIndex。

  • 代码@2:如果从节点存储的最大日志序号小于主节点的最小序号,或者从节点的最小日志序号大于主节点的最大日志序号,即两者不相交,这通常发生在从节点崩溃很长一段时间,而主节点删除了过期的条目时。truncateIndex 设置为主节点的 ledgerBeginIndex,即主节点目前最小的偏移量。

  • 代码@3:如果已比较的日志序号小于从节点的开始日志序号,很可能是从节点磁盘发送损耗,从主节点最小日志序号开始同步。

  • 代码@4:如果已比较的日志序号大于从节点的最大日志序号,则已比较索引设置为从节点最大的日志序号,触发数据的继续同步。

  • 代码@5:如果已比较的日志序号大于从节点的开始日志序号,但小于从节点的最大日志序号,则待比较索引减一。

  • 代码@6:如果比较出来的日志序号小于主节点的最小日志需要,则设置为主节点的最小序号。

if (truncateIndex != -1) {

changeState(truncateIndex, PushEntryRequest.Type.TRUNCATE);

doTruncate(truncateIndex);

break;

}

Step5:如果比较出来的日志序号不等于 -1 ,则向从节点发送 TRUNCATE 请求。

2.3.3.1 doTruncate 详解

private void doTruncate(long truncateIndex) throws Exception {

PreConditions.check(type.get() == PushEntryRequest.Type.TRUNCATE, DLedgerResponseCode.UNKNOWN);

DLedgerEntry truncateEntry = dLedgerStore.get(truncateIndex);

PreConditions.check(truncateEntry != null, DLedgerResponseCode.UNKNOWN);

logger.info(“[Push-{}]Will push data to truncate truncateIndex={} pos={}”, peerId, truncateIndex, truncateEntry.getPos());

PushEntryRequest truncateRequest = buildPushRequest(truncateEntry, PushEntryRequest.Type.TRUNCATE);

PushEntryResponse truncateResponse = dLedgerRpcService.push(truncateRequest).get(3, TimeUnit.SECONDS);

PreConditions.check(truncateResponse != null, DLedgerResponseCode.UNKNOWN, “truncateIndex=%d”, truncateIndex);

PreConditions.check(truncateResponse.getCode() == DLedgerResponseCode.SUCCESS.getCode(), DLedgerResponseCode.valueOf(truncateResponse.getCode()), “truncateIndex=%d”, truncateIndex);

lastPushCommitTimeMs = System.currentTimeMillis();

changeState(truncateIndex, PushEntryRequest.Type.APPEND);

}

该方法主要就是构建 truncate 请求到从节点。

关于服务端的消息复制转发就介绍到这里了,主节点负责向从服务器PUSH请求,从节点自然而然的要处理这些请求,接下来我们就按照主节点发送的请求,来具体分析一下从节点是如何响应的。

3、EntryHandler 详解


EntryHandler 同样是一个线程,当节点状态为从节点时激活。

3.1 核心类图

在这里插入图片描述

其核心属性如下:

  • long lastCheckFastForwardTimeMs

上一次检查主服务器是否有 push 消息的时间戳。

  • ConcurrentMap<Long, Pair<PushEntryRequest, CompletableFuture< PushEntryResponse>>> writeRequestMap

append 请求处理队列。

  • BlockingQueue<Pair<PushEntryRequest, CompletableFuture< PushEntryResponse>>> compareOrTruncateRequests

COMMIT、COMPARE、TRUNCATE 相关请求

3.2 handlePush

从上文得知,主节点会主动向从节点传播日志,从节点会通过网络接受到请求数据进行处理,其调用链如图所示:

在这里插入图片描述

最终会调用 EntryHandler 的 handlePush 方法。

EntryHandler#handlePush

public CompletableFuture handlePush(PushEntryRequest request) throws Exception {

//The timeout should smaller than the remoting layer’s request timeout

CompletableFuture future = new TimeoutFuture<>(1000); // @1

switch (request.getType()) {

case APPEND: // @2

PreConditions.check(request.getEntry() != null, DLedgerResponseCode.UNEXPECTED_ARGUMENT);

long index = request.getEntry().getIndex();

Pair<PushEntryRequest, CompletableFuture> old = writeRequestMap.putIfAbsent(index, new Pair<>(request, future));

if (old != null) {

logger.warn(“[MONITOR]The index {} has already existed with {} and curr is {}”, index, old.getKey().baseInfo(), request.baseInfo());

future.complete(buildResponse(request, DLedgerResponseCode.REPEATED_PUSH.getCode()));

}

break;

case COMMIT: // @3

compareOrTruncateRequests.put(new Pair<>(request, future));

break;

case COMPARE:

case TRUNCATE: // @4

PreConditions.check(request.getEntry() != null, DLedgerResponseCode.UNEXPECTED_ARGUMENT);

writeRequestMap.clear();

compareOrTruncateRequests.put(new Pair<>(request, future));

break;

default:

logger.error(“[BUG]Unknown type {} from {}”, request.getType(), request.baseInfo());

future.complete(buildResponse(request, DLedgerResponseCode.UNEXPECTED_ARGUMENT.getCode()));

break;

}

return future;

}

从几点处理主节点的 push 请求,其实现关键点如下。

代码@1:首先构建一个响应结果Future,默认超时时间 1s。

代码@2:如果是 APPEND 请求,放入到 writeRequestMap 集合中,如果已存在该数据结构,说明主节点重复推送,构建返回结果,其状态码为 REPEATED_PUSH。放入到 writeRequestMap 中,由 doWork 方法定时去处理待写入的请求。

代码@3:如果是提交请求, 将请求存入 compareOrTruncateRequests 请求处理中,由 doWork 方法异步处理。

代码@4:如果是 COMPARE 或 TRUNCATE 请求,将待写入队列 writeRequestMap 清空,并将请求放入 compareOrTruncateRequests 请求队列中,由 doWork 方法异步处理。

接下来,我们重点来分析 doWork 方法的实现。

3.3 doWork 方法详解

EntryHandler#doWork

public void doWork() {

try {

if (!memberState.isFollower()) { // @1

waitForRunning(1);

return;

}

if (compareOrTruncateRequests.peek() != null) { // @2

Pair<PushEntryRequest, CompletableFuture> pair = compareOrTruncateRequests.poll();

PreConditions.check(pair != null, DLedgerResponseCode.UNKNOWN);

switch (pair.getKey().getType()) {

case TRUNCATE:

handleDoTruncate(pair.getKey().getEntry().getIndex(), pair.getKey(), pair.getValue());

break;

case COMPARE:

handleDoCompare(pair.getKey().getEntry().getIndex(), pair.getKey(), pair.getValue());

break;

case COMMIT:

handleDoCommit(pair.getKey().getCommitIndex(), pair.getKey(), pair.getValue());

break;

default:

break;

}

} else { // @3

long nextIndex = dLedgerStore.getLedgerEndIndex() + 1;

Pair<PushEntryRequest, CompletableFuture> pair = writeRequestMap.remove(nextIndex);

if (pair == null) {

checkAbnormalFuture(dLedgerStore.getLedgerEndIndex());

waitForRunning(1);

return;

}

PushEntryRequest request = pair.getKey();

handleDoAppend(nextIndex, request, pair.getValue());

}

} catch (Throwable t) {

DLedgerEntryPusher.logger.error(“Error in {}”, getName(), t);

DLedgerUtils.sleep(100);

}

}

代码@1:如果当前节点的状态不是从节点,则跳出。

代码@2:如果 compareOrTruncateRequests 队列不为空,说明有COMMIT、COMPARE、TRUNCATE 等请求,这类请求优先处理。值得注意的是这里使用是 peek、poll 等非阻塞方法,然后根据请求的类型,调用对应的方法。稍后详细介绍。

代码@3:如果只有 append 类请求,则根据当前节点最大的消息序号,尝试从 writeRequestMap 容器中,获取下一个消息复制请求(ledgerEndIndex + 1) 为 key 去查找。如果不为空,则执行 doAppend 请求,如果为空,则调用 checkAbnormalFuture 来处理异常情况。

接下来我们来重点分析各个处理细节。

3.3.1 handleDoCommit

处理提交请求,其处理比较简单,就是调用 DLedgerStore 的 updateCommittedIndex 更新其已提交偏移量,故我们还是具体看一下DLedgerStore 的 updateCommittedIndex 方法。

DLedgerMmapFileStore#updateCommittedIndex

public void updateCommittedIndex(long term, long newCommittedIndex) { // @1

if (newCommittedIndex == -1

|| ledgerEndIndex == -1

|| term < memberState.currTerm()

|| newCommittedIndex == this.committedIndex) { // @2

return;

}

if (newCommittedIndex < this.committedIndex

|| newCommittedIndex < this.ledgerBeginIndex) { // @3

logger.warn(“[MONITOR]Skip update committed index for new={} < old={} or new={} < beginIndex={}”, newCommittedIndex, this.committedIndex, newCommittedIndex, this.ledgerBeginIndex);

return;

}

long endIndex = ledgerEndIndex;

if (newCommittedIndex > endIndex) { // @4

//If the node fall behind too much, the committedIndex will be larger than enIndex.

newCommittedIndex = endIndex;

}

DLedgerEntry dLedgerEntry = get(newCommittedIndex); // @5

PreConditions.check(dLedgerEntry != null, DLedgerResponseCode.DISK_ERROR);

this.committedIndex = newCommittedIndex;

this.committedPos = dLedgerEntry.getPos() + dLedgerEntry.getSize(); // @6

}

代码@1:首先介绍一下方法的参数:

  • long term

主节点当前的投票轮次。

  • long newCommittedIndex:

主节点发送日志复制请求时的已提交日志序号。

代码@2:如果待更新提交序号为 -1 或 投票轮次小于从节点的投票轮次或主节点投票轮次等于从节点的已提交序号,则直接忽略本次提交动作。

代码@3:如果主节点的已提交日志序号小于从节点的已提交日志序号或待提交序号小于当前节点的最小有效日志序号,则输出警告日志[MONITOR],并忽略本次提交动作。

代码@4:如果从节点落后主节点太多,则重置 提交索引为从节点当前最大有效日志序号。

代码@5:尝试根据待提交序号从从节点查找数据,如果数据不存在,则抛出 DISK_ERROR 错误。

代码@6:更新 commitedIndex、committedPos 两个指针,DledgerStore会定时将已提交指针刷入 checkpoint 文件,达到持久化 commitedIndex 指针的目的。

3.3.2 handleDoCompare

处理主节点发送过来的 COMPARE 请求,其实现也比较简单,最终调用 buildResponse 方法构造响应结果。

EntryHandler#buildResponse

private PushEntryResponse buildResponse(PushEntryRequest request, int code) {

PushEntryResponse response = new PushEntryResponse();

response.setGroup(request.getGroup());

response.setCode(code);

response.setTerm(request.getTerm());

if (request.getType() != PushEntryRequest.Type.COMMIT) {

response.setIndex(request.getEntry().getIndex());

}

response.setBeginIndex(dLedgerStore.getLedgerBeginIndex());

response.setEndIndex(dLedgerStore.getLedgerEndIndex());

return response;

}

主要也是返回当前从几点的 ledgerBeginIndex、ledgerEndIndex 以及投票轮次,供主节点进行判断比较。

3.3.3 handleDoTruncate

handleDoTruncate 方法实现比较简单,删除从节点上 truncateIndex 日志序号之后的所有日志,具体调用dLedgerStore 的 truncate 方法,由于其存储与 RocketMQ 的存储设计基本类似故本文就不在详细介绍,简单介绍其实现要点:根据日志序号,去定位到日志文件,如果命中具体的文件,则修改相应的读写指针、刷盘指针等,并将所在在物理文件之后的所有文件删除。大家如有兴趣,可以查阅笔者的《RocketMQ技术内幕》第4章:RocketMQ 存储相关内容。

3.3.4 handleDoAppend

private void handleDoAppend(long writeIndex, PushEntryRequest request,

CompletableFuture future) {

try {

PreConditions.check(writeIndex == request.getEntry().getIndex(), DLedgerResponseCode.INCONSISTENT_STATE);

DLedgerEntry entry = dLedgerStore.appendAsFollower(request.getEntry(), request.getTerm(), request.getLeaderId());

PreConditions.check(entry.getIndex() == writeIndex, DLedgerResponseCode.INCONSISTENT_STATE);

future.complete(buildResponse(request, DLedgerResponseCode.SUCCESS.getCode()));

dLedgerStore.updateCommittedIndex(request.getTerm(), request.getCommitIndex());

} catch (Throwable t) {

logger.error(“[HandleDoWrite] writeIndex={}”, writeIndex, t);

future.complete(buildResponse(request, DLedgerResponseCode.INCONSISTENT_STATE.getCode()));

}

}

其实现也比较简单,调用DLedgerStore 的 appendAsFollower 方法进行日志的追加,与appendAsLeader 在日志存储部分相同,只是从节点无需再转发日志。

3.3.5 checkAbnormalFuture

该方法是本节的重点,doWork 的从服务器存储的最大有效日志序号(ledgerEndIndex) + 1 序号,尝试从待写请求中获取不到对应的请求时调用,这种情况也很常见,例如主节点并么有将最新的数据 PUSH 给从节点。接下来我们详细来看看该方法的实现细节。

EntryHandler#checkAbnormalFuture

if (DLedgerUtils.elapsed(lastCheckFastForwardTimeMs) < 1000) {

return;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

Spring全套教学资料

Spring是Java程序员的《葵花宝典》,其中提供的各种大招,能简化我们的开发,大大提升开发效率!目前99%的公司使用了Spring,大家可以去各大招聘网站看一下,Spring算是必备技能,所以一定要掌握。

目录:

部分内容:

Spring源码

  • 第一部分 Spring 概述
  • 第二部分 核心思想
  • 第三部分 手写实现 IoC 和 AOP(自定义Spring框架)
  • 第四部分 Spring IOC 高级应用
    基础特性
    高级特性
  • 第五部分 Spring IOC源码深度剖析
    设计优雅
    设计模式
    注意:原则、方法和技巧
  • 第六部分 Spring AOP 应用
    声明事务控制
  • 第七部分 Spring AOP源码深度剖析
    必要的笔记、必要的图、通俗易懂的语言化解知识难点

脚手框架:SpringBoot技术

它的目标是简化Spring应用和服务的创建、开发与部署,简化了配置文件,使用嵌入式web服务器,含有诸多开箱即用的微服务功能,可以和spring cloud联合部署。

Spring Boot的核心思想是约定大于配置,应用只需要很少的配置即可,简化了应用开发模式。

  • SpringBoot入门
  • 配置文件
  • 日志
  • Web开发
  • Docker
  • SpringBoot与数据访问
  • 启动配置原理
  • 自定义starter

微服务架构:Spring Cloud Alibaba

同 Spring Cloud 一样,Spring Cloud Alibaba 也是一套微服务解决方案,包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

  • 微服务架构介绍
  • Spring Cloud Alibaba介绍
  • 微服务环境搭建
  • 服务治理
  • 服务容错
  • 服务网关
  • 链路追踪
  • ZipKin集成及数据持久化
  • 消息驱动
  • 短信服务
  • Nacos Confifig—服务配置
  • Seata—分布式事务
  • Dubbo—rpc通信

Spring MVC

目录:

部分内容:

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
外链图片转存中…(img-qlDPcwSK-1713750638882)]

Spring源码

  • 第一部分 Spring 概述
  • 第二部分 核心思想
  • 第三部分 手写实现 IoC 和 AOP(自定义Spring框架)
  • 第四部分 Spring IOC 高级应用
    基础特性
    高级特性
  • 第五部分 Spring IOC源码深度剖析
    设计优雅
    设计模式
    注意:原则、方法和技巧
  • 第六部分 Spring AOP 应用
    声明事务控制
  • 第七部分 Spring AOP源码深度剖析
    必要的笔记、必要的图、通俗易懂的语言化解知识难点

[外链图片转存中…(img-8mofw0P4-1713750638883)]

[外链图片转存中…(img-V6QKGV4U-1713750638883)]

脚手框架:SpringBoot技术

它的目标是简化Spring应用和服务的创建、开发与部署,简化了配置文件,使用嵌入式web服务器,含有诸多开箱即用的微服务功能,可以和spring cloud联合部署。

Spring Boot的核心思想是约定大于配置,应用只需要很少的配置即可,简化了应用开发模式。

  • SpringBoot入门
  • 配置文件
  • 日志
  • Web开发
  • Docker
  • SpringBoot与数据访问
  • 启动配置原理
  • 自定义starter

[外链图片转存中…(img-lw8hpaF4-1713750638883)]

[外链图片转存中…(img-9wk9DHjE-1713750638883)]

微服务架构:Spring Cloud Alibaba

同 Spring Cloud 一样,Spring Cloud Alibaba 也是一套微服务解决方案,包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

  • 微服务架构介绍
  • Spring Cloud Alibaba介绍
  • 微服务环境搭建
  • 服务治理
  • 服务容错
  • 服务网关
  • 链路追踪
  • ZipKin集成及数据持久化
  • 消息驱动
  • 短信服务
  • Nacos Confifig—服务配置
  • Seata—分布式事务
  • Dubbo—rpc通信

[外链图片转存中…(img-8wAycXMB-1713750638883)]

[外链图片转存中…(img-RUUryANw-1713750638883)]

Spring MVC

目录:

[外链图片转存中…(img-UZdTj26K-1713750638884)]

[外链图片转存中…(img-pQIuCK9C-1713750638884)]

[外链图片转存中…(img-qoaKYd7V-1713750638884)]

部分内容:

[外链图片转存中…(img-tWWm5ejQ-1713750638884)]

[外链图片转存中…(img-4rq4nT91-1713750638884)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值