ElasticSearch读流程
目录 [隐藏]
- GET 流程
- 内容路由,获取shardit过程
- 协调请求过程
- 本地节点数据读取和发送流程
- MGET流程
- 通过分析读流程,我们思考以下问题:
- GET 相关参数
基于版本:2.3.2
这次分析的读流程指 GET/MGET 过程,不包含搜索过程。
GET/MGET 必须指定三元组: index type id。 type 可以使用 _all 表示从所有 type 获取第一个匹配 id 的 doc。
mget 时, type 留空表示 _all,例如可以这样:
1
2
3
|
GET /website/_mget
|
GET 则必须明确指定 _all ,例如必须这样:
1
2
3
|
GET /website/blog/1
|
而不能
1
2
3
|
GET /website/1
|
GET 流程
整体分为五个阶段:准备集群信息,内容路由,协调请求,数据读取,回复客户端。
在处理入口,根据action字符串获取对应的TransportAction实现类,对于一个单个doc的get请求,获取到的是一个
1
2
3
|
TransportSingleShardAction TransportAction<Request, Response> transportAction = actions.get(action);
|
一个 TransportSingleShardAction 对象用来处理存在于一个单个主分片或者副本分片上的读请求。
准备集群信息
1.在 TransportSingleShardAction 构造函数中,已准备好 clusterState、nodes 列表等信息
2.resolveRequest函数从ClusterState中获取IndexMetaData,更新可能存在的自定义routing信息
内容路由
确定目标节点,获取shard迭代器,其中包含了目的node信息
1
2
3
4
5
6
7
8
9
10
11
12
13
|
private AsyncSingleAction(Request request, ActionListener<Response> listener) {
ClusterState clusterState = clusterService.state();
//集群nodes列表
nodes = clusterState.nodes();
resolveRequest(clusterState, internalRequest);
//根据hash和shard数量取余计算一个随机目的shard,或者走优先级规则
this.shardIt = shards(clusterState, internalRequest);
}
|
作协调请求,向目标节点发送请求,处理响应,回复客户端,主要代码如下 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
private void perform(@Nullable final Throwable currentFailure) {
DiscoveryNode node = nodes.get(shardRouting.currentNodeId());
if (node == null) {
onFailure(shardRouting, new NoShardAvailableActionException(shardRouting.shardId()));
} else {
internalRequest.request().internalShardId = shardRouting.shardId();
transportService.sendRequest(node, transportShardAction, internalRequest.request(), new BaseTransportResponseHandler<Response>() {
//上面的 sendRequest 不管是发送到网络还是由本地节点直接处理的,下面的函数用于处理后续的响应操作
@Override
public void handleResponse(final Response response) {
listener.onResponse(response);
}
@Override
public void handleException(TransportException exp) {
onFailure(shardRouting, exp);
}
});
}
}
|
代码入口:
rest请求接受和处理的类位于:
HttpRequestHandler::messageReceived
接收到请求后根据action获取handle,调用不同handle进行处理的的实现位于:RequestHandlerRegistry::processMessageReceived
单个shard读请求处理实现位于:
TransportSingleShardAction::messageReceived
index 读取的核心实现位于:
InternalEngine::get
内容路由,获取shardit过程
shardit 是一个List的迭代器,默认情况下是所有 activeShard 中随机选择的一个位置的迭代器,如果存在优先级参数会有些其他的过滤条件.ShardRouting类是对一个独一无二的 shard 相关的路由信息.因此这个环节就是要确定最终目的 shard 是哪个,他的相关节点是哪个。
调用OperationRouting::getShards实现
1
2
3
4
5
|
public ShardIterator getShards(ClusterState clusterState, String index, String type, String id, @Nullable String routing, @Nullable String preference) {
return preferenceActiveShardIterator(shards(clusterState, index, type, id, routing), clusterState.nodes().localNodeId(), clusterState.nodes(), preference);
}
|
1.计算 shardid,然后从路由表获取匹配index 和 shard 的activeShardsgenerateShardId()通过对id等进行hash,对主分片取余,获得目的shardid,然后获取 shardid 对应的内容路由表.期间,会检查索引是否存在,不存在则抛异常。
1
2
3
4
5
6
|
protected IndexShardRoutingTable shards(ClusterState clusterState, String index, String type, String id, String routing) {
int shardId = generateShardId(clusterState, index, type, id, routing);
return clusterState.getRoutingTable().shardRoutingTable(index, shardId);
}
|
其中内容路由规则:
generateShardId中实现
1
2
3
|
shard = hash(routing) % number_of_primary_shards
|
routing 是一个可变值,默认是文档的 _id ,另外可以根据routing指定的值,或者同时参考id与type
读取的时候也是这样hash计算出的目的shard
2.从 activeShard s 中选择目标.调用OperationRouting::preferenceActiveShardIterator()实现后续流程。首先检查是否存在优先级:preference如果不存在,调用ctiveInitializingShardsRandomIt();从activeshards中返回一个随机的node,随机算法在CollectionUtils.rotate实现,只是用一个随机数对activeShards.size()取余如果请求中存在优先级设置,进入分片查询优先级判断逻辑,优先级算法只是将对 activeShard 的随机选择改成了按一定条件把某个shard 放到List 最前面,然后返回第一个.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
private ShardIterator preferenceActiveShardIterator(IndexShardRoutingTable indexShard, String localNodeId, DiscoveryNodes nodes, @Nullable String preference) {
if (preference == null || preference.isEmpty()) {
String[] awarenessAttributes = awarenessAllocationDecider.awarenessAttributes();
if (awarenessAttributes.length == 0) {
return indexShard.activeInitializingShardsRandomIt();
} else {
return indexShard.preferAttributesActiveInitializingShardsIt(awarenessAttributes, nodes);
}
}
if (preference.charAt(0) == '_') {
..
}
}
|
协调请求过程
本节点作为协调节点,向目标 node 转发请求,或者目标是本地节点,直接通过函数调用读取数据.发送流程封装了对请求的发送,并且声明了如何对 Response 进行处理:AsyncSingleAction 类中声明的对 Response 进行处理的函数,无论请求在本节点处理还是发送到其他节点,都会经过这个函数处理:
1
2
3
4
5
|
public void handleResponse(final Response response) {
listener.onResponse(response);
}
|
最终调用到给客户端回复 Response ,在RestResponseListener类发送:
1
2
3
4
5
|
protected final void processResponse(Response response) throws Exception {
channel.sendResponse(buildResponse(response));
}
|
下面看下具体过程:
1.TransportService::sendRequest中检查目标是否本地node
1
2
3
4
5
6
7
|
if (node.equals(localNode)) {
sendLocalRequest(requestId, action, request);
} else {
transport.sendRequest(node, requestId, action, request, options);
}
|
2.如果是本地node,进入TransportService::sendLocalRequest流程sendLocalRequest不发送到网络,直接根据action获取注册的reg,执行processMessageReceived
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//sendRequest发现目标node是本地时(if (node.equals(localNode))),调用到本函数
private void sendLocalRequest(long requestId, final String action, final TransportRequest request) {
final DirectResponseChannel channel = new DirectResponseChannel(logger, localNode, action, requestId, adapter, threadPool);
try {
final RequestHandlerRegistry reg = adapter.getRequestHandler(action);
final String executor = reg.getExecutor();
if (ThreadPool.Names.SAME.equals(executor)) {
//noinspection unchecked
reg.processMessageReceived(request, channel);
}
.......
}
|
3.进入数据读取流程
4.如果是发送到网络,请求被异步发送,sendRequest的时候注册 handle:
1
2
3
|
transportService.sendRequest(node, transportShardAction, internalRequest.request(), new BaseTransportResponseHandler<Response>()
|
在TransportService::sendRequest中,这个 handle 最终被 添加到
1
2
3
|
clientHandlers: clientHandlers.put(requestId, new RequestHolder<>(handler, node, action, timeoutHandler));
|
然后,设置超时,等待处理 Response:
1
2
3
4
5
6
7
8
9
10
|
public TransportResponseHandler onResponseReceived(final long requestId) {
RequestHolder holder = clientHandlers.remove(requestId);
holder.cancelTimeout();
if (traceEnabled() && shouldTraceAction(holder.action())) {
traceReceivedResponse(requestId, holder.node(), holder.action());
}
return holder.handler();
}
|
收到其他节点的 Response 后,通过之前声明的handleResponse,给客户端返回响应内容.
本地节点数据读取和发送流程
RequestHandlerRegistry::processMessageReceived作为Request消息处理的总入口,根据action获取handle,调用对应的handler.messageReceived
进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//对于所有Request消息处理的入口
public void processMessageReceived(Request request, TransportChannel channel) throws Exception {
final Task task = taskManager.register(channel.getChannelType(), action, request);
if (task == null) {
handler.messageReceived(request, channel);
} else {
boolean success = false;
try {
handler.messageReceived(request, new TransportChannelWrapper(taskManager, task, channel), task);
success = true;
} finally {
if (success == false) {
taskManager.unregister(task);
}
}
}
}
|
对于单个shard的读请求,进入
1
2
3
|
TransportSingleShardAction::ShardTransportHandler::messageReceived()
|
读取数据组织成 Response, 给客户端 channel 返回。
1
2
3
4
5
6
|
public void messageReceived(final Request request, final TransportChannel channel) throws Exception {
Response response = shardOperation(request, request.internalShardId);
channel.sendResponse(response);
}
|
shardOperation主要处理请求中是否有 refresh 选项,然后调用indexShard.getService().get() 读取数据,存储到 GetResult.为什么需要在 realtime 未开启的状态下 refresh 选项才能生效呢?如果一个GET操作要求先刷新数据,以此实现实时读取,这意味着数据从 lucene 获取,不走 translog.那他确实没必要开启 realtime 选项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
protected GetResponse shardOperation(GetRequest request, ShardId shardId) {
IndexService indexService = indicesService.indexServiceSafe(shardId.getIndex());
IndexShard indexShard = indexService.shardSafe(shardId.id());
if (request.refresh() && !request.realtime()) {
indexShard.refresh("refresh_flag_get");
}
GetResult result = indexShard.getService().get(request.type(), request.id(), request.fields(),
request.realtime(), request.version(), request.versionType(), request.fetchSourceContext(), request.ignoreErrorsOnGeneratedFields());
return new GetResponse(result);
}
|
ShardGetService::get()中,调用GetResult getResult = innerGet()获取到结果.
GetResult类用于存储读取到的真实数据内容.而 Engine::GetResult类封装的是响应的 lucene IndexSearch 和translog 等信息因此,核心的数据读取实现在ShardGetService::innerGet()函数中.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private GetResult innerGet(String type, String id, String[] gFields, boolean realtime, long version, VersionType versionType, FetchSourceContext fetchSourceContext, boolean ignoreErrorsOnGeneratedFields) {
fetchSourceContext = normalizeFetchSourceContent(fetchSourceContext, gFields);
Engine.GetResult get = null;
if (type == null || type.equals("_all")) {
...
} else {
get = indexShard.get(new Engine.Get(realtime, new Term(UidFieldMapper.NAME, Uid.createUidAsBytes(type, id)))
.version(version).versionType(versionType));
...
}
DocumentMapper docMapper = mapperService.documentMapper(type);
try {
// break between having loaded it from translog (so we only have _source), and having a document to load
if (get.docIdAndVersion() != null) {
return innerGetLoadFromStoredFields(type, id, gFields, fetchSourceContext, get, docMapper, ignoreErrorsOnGeneratedFields);
}
...
}
}
|
1.首先,通过indexShard.get()获取Engine.GetResult,里面有重要的 lucene indexsearch,或者Translog.Source 等信息。
get()函数最终实现在InternalEngine::get()
先获取读锁:
1
2
3
|
try (ReleasableLock lock = readLock.acquire())
|
如果是 数据位于本机的最新数据(versionMap中存在),则从 translog 获取.非realtime通过 lucene 获取,如果指定了 version 且 version 不存在,读取失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public GetResult get(Get get) throws EngineException {
try (ReleasableLock lock = readLock.acquire()) {
ensureOpen();
if (get.realtime()) {
//versionMap 中的值是写入索引的时候添加的,并且不做持久化.
VersionValue versionValue = versionMap.getUnderLock(get.uid().bytes());
//一般不进入下面的 if, 有两个条件:1.最近写入的数据(具体多新未知),2,读取的时候指定了 version
if (versionValue != null) {
if (versionValue.delete()) {//删除标识,数据已通过 delete 接口删除了
return GetResult.NOT_EXISTS;
}
if (get.versionType().isVersionConflictForReads(versionValue.version(), get.version())) {
Uid uid = Uid.createUid(get.uid().text());
throw new VersionConflictEngineException(shardId, uid.type(), uid.id(), versionValue.version(), get.version());
}
Translog.Operation op = translog.read(versionValue.translogLocation());
if (op != null) {
return new GetResult(true, versionValue.version(), op.getSource());
}
}
}
// no version, get the version from the index, we know that we refresh on flush
return getFromSearcher(get);
}
}
|
2.调用ShardGetService::innerGetLoadFromStoredFields(),根据 type,id,DocumentMapper 等信息从刚刚get 到的信息中获取数据,对指定的field,source,进行过滤(source 过滤只支持对字段),把结果存于GetResult对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
private GetResult innerGetLoadFromStoredFields(String type, String id, String[] gFields, FetchSourceContext fetchSourceContext, Engine.GetResult get, DocumentMapper docMapper, boolean ignoreErrorsOnGeneratedFields) {
Map<String, GetField> fields = null;
BytesReference source = null;
Versions.DocIdAndVersion docIdAndVersion = get.docIdAndVersion();
FieldsVisitor fieldVisitor = buildFieldsVisitors(gFields, fetchSourceContext);
if (fieldVisitor != null) {
try {
docIdAndVersion.context.reader().document(docIdAndVersion.docId, fieldVisitor);
} catch (IOException e) {
throw new ElasticsearchException("Failed to get type [" + type + "] and id [" + id + "]", e);
}
source = fieldVisitor.source();
...
}
return new GetResult(shardId.index().name(), type, id, get.version(), get.exists(), source, fields);
}
|
MGET流程
mget主要处理类:TransportMultiGetAction,其集成关系如下,通过封装单个 GET 请求实现
主要流程如下:
1.request 的 doc 数目中遍历,计算出由 sharid 为 key 组成的 request map.这个过程不在 TransportSingleShardAction 中实现,
是因为如果在那边实现, shardid 会重复
2.循环处理组织好的每个请求,走TransportSingleShardAction中处理单个 doc 的流程.与处理单个 doc 时相比,只是在构建TransportSingleShardAction对象时,传入的泛型:Request,Response不同.这就是说大约只是 shardid 内部计算还是外部计算等3.收集Response,全部 Response返回后执行finishHim(),给客户端返回结果
总结:回复的消息中 doc 顺序与请求的顺序一致如果部分 doc检索失败,不影响其他结果,检索失败的 doc 会在回复信息中标出
通过分析读流程,我们思考以下问题:
读失败是怎么处理的?
没有重试处理.无论是否指定优先级,都不会尝试重读,优先级只是在处理 avtiveshard 的ArrayList时将匹配
的放到了ArrayList前面而已.
怎么选择从主分片还是副本分片读取的?
从 activeshard 中随机选择,通过指定优先级可以从主分片读
分配是 shardit迭代器, 而不是单个目标 node, 难道想挨个尝试?
没有实现挨个尝试,只是向一个发请求后不管
读请求命中 translog 的条件是什么?
写完数据之后,短时间内发起的读操作(无论读操作到达哪个节点,都可以命中,每个主\副分片所在节点都写了 translog)
为什么要加读锁?怕读的时候有人删了改?需要分布式锁吗?
用于多线程间的同步.不需要分布式锁,只锁本节点本进程即可.设想 A 节点在读, B 节点要删除,B 节点删除成功, B 作为协调节点向 A 发送删除请求,该请求会阻塞在读写锁.锁的范围:每个shard有一个读写锁.读写锁是Engine类的成员变量,集群启动的时候, 为存储于本地节点的 index::shard创建一个Engine对象.循环位于:IndicesClusterStateService::applyNewOrUpdatedShards()
读取指定 route 如何处理的?
对GET 请求中 routing 参数的的处理,就是 把默认对 id 进行 hash, 改为对 routing 指定的值进行 hash
对 _source,_field 等过滤器如何处理的?在哪个环节处理的?是否 lucene 处理的?全部读取出来之后才做的 filter 吗?
_source,_field 是在读取了完整的 doc 之后在innerGetLoadFromStoredFields函数中做过滤的
refresh参数在哪实现的?
TransportGetAction::shardOperation()函数,设置 refesh 为 true,并且关闭 realtime 才会刷新 shard.
cache 机制是如何的?
早期版本缓存一切可以缓存的数据
使用频率较高,数据量较大的才进行缓存
缓存老化算法为 LRU: 最近最少使用
参考:
https://www.elastic.co/guide/en/elasticsearch/guide/current/filter-caching.htmlhttps://www.elastic.co/guide/cn/elasticsearch/guide/current/filter-caching.html
读取操作是实时还是准实时?
实时指写入完成后立刻读取,是否能读到。
读取是实时的。因为三元组明确,不需要走倒排索引
search 给的的是关键词,必须走倒排才能查到,不走 translog, 所以是近实时的.
参考:
https://www.elastic.co/guide/en/elasticsearch/guide/current/near-real-time.htmlhttps://www.elastic.co/guide/en/elasticsearch/reference/2.3/docs-get.html#realtimeGET/MGET
为什么不默认读本地节点?
??
GET 相关参数
realtime
默认开启2.3.2版本:尝试从 translog 读取.命中条件:写完数据之后,短时间内发起的读操作会命中,无论这个读取操作被发到哪个节点.因为当一个写操作返回时,所有主,副分片所在节点都有 translog 可以命中5.5的版本中,不受索引刷新速率的影响,如果一个document没有被更新了,但是还没有刷新,那么get API获取此文档的时候会先刷新,然后再get
Optional Type
如果想要查询所有的类型,可以直接指定_type为_all,从而匹配所有的类型。返回匹配 id 的第一个 doc
Source filtering
默认情况下get操作会返回_source字段,除非你使用了fields字段或者禁用了_source字段。通过设置_source属性,可以禁止返回source内容:
1
2
3
|
curl -XGET 'http://localhost:9200/twitter/tweet/1?_source=false'
|
如果想要返回特定的字段,可以使用_source_include或者_source_exclude进行过滤。可以使用逗号分隔来设置多种匹配模式,比如:
1
2
3
|
curl -XGET 'http://localhost:9200/twitter/tweet/1?_source_include=*.id&_source_exclude=entities'
|
如果希望返回特定的字段,也可以直接写上字段的名称:
1
2
3
|
curl -XGET 'http://localhost:9200/twitter/tweet/1?_source=*.id,retweeted'
|
Fields
get操作允许设置fields字段,返回特定的字段:
curl -XGET ‘http://localhost:9200/twitter/tweet/1?fields=title,content’
如果请求的字段没有被存储,那么他们会从source中分析出来,这个功能也可以用 source filter来替代。
元数据比如_routing和_parent是永远不会被返回的。
只有叶子字段才能通过field选项返回.所以对象字段这种是不能返回的,这种请求也会失败。
Routing
当索引的时候指定了路由,那么查询的时候就一定要指定路由。
curl -XGET ‘http://localhost:9200/twitter/tweet/1?routing=kimchy’
如果路由信息不正确,就会查找不到文档
Preference
控制为get请求维护一个分片的索引,这个索引可以设置为:
_primary 这个操作仅仅会在主分片上执行。
_local 这个操作会在本地的分片上执行。
Custom (string) value 用户可以自定义值,对于相同的分片可以设置相同的值。这样可以保证不同的刷新状态下,查询不同的分片。就像sessionid或者用户名一样。
Refresh
refresh参数可以让每次get之前都刷新分片,使这个值可以被搜索。设置true的时候,尽量要考虑下性能问题,因为每次刷新都会给系统带来一定的压力
Versioning support
使用 version 参数检索文档,只有当前版本号与指定版本号相同时才能成功,这种机制对所有版本类型都有效,除了FOUCE, 他总是返回 doc.
ref:
http://blog.csdn.net/u010994304/article/details/50441419
http://www.code123.cc/2582.html
http://www.jianshu.com/p/62febe581fcb
http://blog.csdn.net/july_2/article/details/24777931
http://blog.csdn.net/laigood/article/details/8450331
http://www.cnblogs.com/xing901022/p/5317698.html
ElasticSearch读流程相关推荐
- Rocksdb 写流程,读流程,WAL文件,MANIFEST文件,ColumnFamily,Memtable,SST文件原理详解
文章目录 前言 Rocksdb写流程图 WAL 原理分析 概述 文件格式 查看WAL的工具 创建WAL 清理WAL MANIFEST原理分析 概述 查看MANIFEST的工具 创建 及 清除 MANI ...
- 【Elasticsearch】Elasticsearch gateway 流程分析
1.概述 转载:elasticsearch gateway 流程分析 es 存储的数据有以下几种形式: state index translog index 为 lucene 生成的索引文件 tran ...
- HDFS读流程,写流程,放置策略
1.HDFS写流程 [hadoop@hadoop002 hadoop-2.6.0-cdh5.7.0]$ hdfs dfs -put LICENSE.txt / 19/02/20 21:30:22 WA ...
- F2FS源码分析-2.3 [F2FS 读写部分] F2FS的一般文件读流程分析
F2FS源码分析系列文章 主目录 一.文件系统布局以及元数据结构 二.文件数据的存储以及读写 F2FS文件数据组织方式 一般文件写流程 一般文件读流程 目录文件读流程(未完成) 目录文件写流程(未完成 ...
- HBase2.x(十一)HBase 读流程
文章目录 HFile 结构 读流程 合并读取数据优化 HFile 结构 在了解读流程之前,需要先知道读取的数据是什么样子的. HFile 是存储在 HDFS 上面每一个 store 文件夹下实际存储数 ...
- Hadoop-HDFS(一)读流程
个人理解,各位大牛可以把自己的理解分享一下!小弟会认真看每一个大牛的留言 HDFS读流程 . 如图(图是别人的)所示:1.使用HDFS提供的客户端Client 向NameNode(个人理解为数据的管理 ...
- TiDB读流程概述,一张图搞明白
tidb的三大模块的功能就不赘述了,先要有这些知识的基础,不然看着肯定会有疑惑: 对比tibd的SQL读流程其实和MySQL的主流程主提一样,因为tidb也是兼容MySQL协议的,如果你多MySQL的 ...
- php操作ElasticSearch搜索引擎流程详解
更多python.php教程请到友情连接: 菜鸟教程https://www.piaodoo.com 茂名一技http://www.enechn.com ppt制作教程步骤 http://www.tpy ...
- elasticsearch全文检索流程
elasticsearch全文检索流程 elasticsearch全文检索流程 索引过程 创建索引 获得原始文档 创建文档对象 分析文档 创建索引 查询索引 elasticsearch全文检索流程 索 ...
- hadoop 读流程和写流程
hadoop HDFD读流程 hadoop HDFD写流程package com.lhj.hadoop;import java.io.BufferedReader; import java.io.IO ...
最新文章
- [JAVA EE] JPA技术基础:完成数据列表的删除
- ATS名词术语(待续)
- Datawhale团队第六期录取名单!
- win10下安装tensorflow-gpu==1.11.0的详细教程
- ehcache 手动刷新缓存_【第 21 期】一个架构师的缓存修炼之路
- linux中高端内存和低端内存的概念【转】
- ubuntu15.04安装wps-office的64位版
- 两阶段网路dea模型matlab实现(支持多种投入产出结构任意组合,支持规模报酬是否可变的调整、两阶段效率权重下限的调整和共享投入分配比例的调整)
- git本地库案例-找回删除的文件
- android数据格式化,手机格式化了?教你找回安卓手机数据
- 4讲 图像 表格 实际应用-菜谱 课堂练习-课程表
- 快速安装AXURE谷歌扩展插件
- Android Studio如何更新至最新版本4.2.2
- 2022 高教杯数学建模C题古代玻璃制品的成分分析与鉴别回顾及总结
- 与消费者情感同频共振,都市丽人荣获TMA移动营销大奖
- Synaptics 蠕虫病毒解决方法
- java 获取下一年_JAVA获取下一年,下个月,下一天;月份为何以0开始?
- 《乡村爱情11》将播 除了刘能赵四,竟还有狄龙
- 自定义控件:图片轮播,点击图片进入webview
- すぬけ君の塗り絵 2 イージー / Snuke's Coloring 2 (ABC Edit) AtCoder - 2145
热门文章
- 百问网7天物联网智能家居 学习心得 打卡第七天
- java蓝桥杯合根植物_Java实现蓝桥杯 历届试题 合根植物
- 桌面计算机右键管理没反应,右键计算机(我的电脑)管理选项打不开解决措施
- 一个Android开发6年程序员的年终面试总结,2021无畏艰难险阻,迎风潇洒前行
- 设置背景图片自动适应屏幕
- js的alert弹框中怎么写html,JavaScript实现alert弹框效果
- SOA联姻IMS对3G无线网络是福是祸?
- math.floor()函数解析
- Axure RP8 进度条
- UCanCode发布跨平台开源组态\ 建模\仿真\工控VX++ 2021