Flink教程(13)- Flink高级API(状态管理)
文章目录
- 01 引言
- 02 Flink中的有状态计算
- 03 有状态和无状态计算
- 3.1 无状态计算
- 3.1.1 无状态计算特点
- 3.1.2 无状态计算例子(消费延迟计算)
- 3.2 有状态计算
- 3.2.1 有状态计算特点
- 3.2.2 有状态计算例子(访问量统计)
- 3.2.3 有状态计算的场景
- 04 状态的分类
- 4.1 Managed State & Raw State
- 4.2 Keyed State & Operator State
- 4.2.1 Keyed State
- 4.2.2 Operator State
- 05 State存储结构
- 5.1 State API
- 06 State 案例
- 6.1 Keyed State案例
- 6.2 Operator State案例
- 07 文末
01 引言
在前面的博客,我们已经对Flink
批流一体API
的使用有了一定的了解了,有兴趣的同学可以参阅下:
- 《Flink教程(01)- Flink知识图谱》
- 《Flink教程(02)- Flink入门》
- 《Flink教程(03)- Flink环境搭建》
- 《Flink教程(04)- Flink入门案例》
- 《Flink教程(05)- Flink原理简单分析》
- 《Flink教程(06)- Flink批流一体API(Source示例)》
- 《Flink教程(07)- Flink批流一体API(Transformation示例)》
- 《Flink教程(08)- Flink批流一体API(Sink示例)》
- 《Flink教程(09)- Flink批流一体API(Connectors示例)》
- 《Flink教程(10)- Flink批流一体API(其它)》
- 《Flink教程(11)- Flink高级API(Window)》
- 《Flink教程(12)- Flink高级API(Time与Watermaker)》
在前面的教程,我们已经学习了Flink
的四大基石里面的Time
了,如下图,本文讲解下State 状态
:
02 Flink中的有状态计算
Flink中已经对需要进行有状态计算的API
做了封装,底层已经维护好了状态!
不需要像SparkStreaming
那样还得自己写updateStateByKey
,也就是说我们今天学习的State
只需要掌握原理,实际开发中一般都是使用Flink
底层维护好的状态或第三方维护好的状态(如Flink
整合Kafka
的offset
维护底层就是使用的State
,但是人家已经写好了的)
/*** 有状态计算** @author : YangLinWei* @createTime: 2022/3/7 11:31 下午*/
public class SourceDemo031 {public static void main(String[] args) throws Exception {//1.envStreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);//2.sourceDataStream<String> linesDS = env.socketTextStream("node1", 9999);//3.处理数据-transformation//3.1每一行数据按照空格切分成一个个的单词组成一个集合DataStream<String> wordsDS = linesDS.flatMap(new FlatMapFunction<String, String>() {@Overridepublic void flatMap(String value, Collector<String> out) throws Exception {//value就是一行行的数据String[] words = value.split(" ");for (String word : words) {out.collect(word);//将切割处理的一个个的单词收集起来并返回}}});//3.2对集合中的每个单词记为1DataStream<Tuple2<String, Integer>> wordAndOnesDS = wordsDS.map(new MapFunction<String, Tuple2<String, Integer>>() {@Overridepublic Tuple2<String, Integer> map(String value) throws Exception {//value就是进来一个个的单词return Tuple2.of(value, 1);}});//3.3对数据按照单词(key)进行分组//KeyedStream<Tuple2<String, Integer>, Tuple> groupedDS = wordAndOnesDS.keyBy(0);KeyedStream<Tuple2<String, Integer>, String> groupedDS = wordAndOnesDS.keyBy(t -> t.f0);//3.4对各个组内的数据按照数量(value)进行聚合就是求sumDataStream<Tuple2<String, Integer>> result = groupedDS.sum(1);//4.输出结果-sinkresult.print();//5.触发执行-executeenv.execute();}
}
执行 netcat,然后在终端输入 hello world,执行程序会输出什么?
- 答案很明显,
(hello, 1)
和(word,1)
那么问题来了,如果再次在终端输入 hello world,程序会输入什么?
- 答案其实也很明显,
(hello, 2)
和(world, 2)
。
因为 Flink
知道之前已经处理过一次 hello world
,这就是 state
发挥作用了,这里是被称为 keyed state
存储了之前需要统计的数据,所以Flink
知道hello
和world
分别出现过一次。
03 有状态和无状态计算
3.1 无状态计算
3.1.1 无状态计算特点
无状态计算的特点:
- 不需要考虑历史数据
- 相同的输入得到相同的输出就是无状态计算,如
map/flatMap/filter
…
3.1.2 无状态计算例子(消费延迟计算)
上图是一个无状态计算的例子,消费延迟计算:假设现在有一个消息队列,消息队列中有一个生产者持续往消费队列写入消息,多个消费者分别从消息队列中读取消息。
从图上可以看出,生产者已经写入 16 条消息,Offset
停留在 15 ;有 3 个消费者,有的消费快,而有的消费慢。消费快的已经消费了 13 条数据,消费者慢的才消费了 7、8 条数据。
如何实时统计每个消费者落后多少条数据,如图给出了输入输出的示例。可以了解到输入的时间点有一个时间戳,生产者将消息写到了某个时间点的位置,每个消费者同一时间点分别读到了什么位置。刚才也提到了生产者写入了 15 条,消费者分别读取了 10、7、12 条。那么问题来了,怎么将生产者、消费者的进度转换为右侧示意图信息呢?
consumer 0
落后了 5 条,consumer 1
落后了 8 条,consumer 2
落后了 3 条,根据 Flink
的原理,此处需进行Map
操作。Map
首先把消息读取进来,然后分别相减,即可知道每个 consumer
分别落后了几条。Map
一直往下发,则会得出最终结果。
大家会发现,在这种模式的计算中,无论这条输入进来多少次,输出的结果都是一样的,因为单条输入中已经包含了所需的所有信息。消费落后等于生产者减去消费者。生产者的消费在单条数据中可以得到,消费者的数据也可以在单条数据中得到,所以相同输入可以得到相同输出,这就是一个无状态的计算。
3.2 有状态计算
3.2.1 有状态计算特点
特点:
- 需要考虑历史数据
- 相同的输入得到不同的输出/不一定得到相同的输出,就是有状态计算,如:
sum/reduce
3.2.2 有状态计算例子(访问量统计)
以访问日志统计量的例子进行说明,比如当前拿到一个 Nginx
访问日志,一条日志表示一个请求,记录该请求从哪里来,访问的哪个地址,需要实时统计每个地址总共被访问了多少次,也即每个 API 被调用了多少次。
可以看到下面简化的输入和输出,输入第一条是在某个时间点请求 GET 了 /api/a
;第二条日志记录了某个时间点 Post /api/b
;第三条是在某个时间点 GET
了一个 /api/a
,总共有 3 个 Nginx
日志。
从这 3 条 Nginx
日志可以看出,第一条进来输出 /api/a
被访问了一次,第二条进来输出 /api/b
被访问了一次,紧接着又进来一条访问api/a
,所以 api/a
被访问了 2 次。
不同的是,两条/api/a
的Nginx
日志进来的数据是一样的,但输出的时候结果可能不同,第一次输出 count=1
,第二次输出count=2
,说明相同输入可能得到不同输出。
输出的结果取决于当前请求的 API
地址之前累计被访问过多少次。第一条过来累计是 0 次,count
= 1,第二条过来API
的访问已经有一次了,所以/api/a
访问累计次数 count
=2。单条数据其实仅包含当前这次访问的信息,而不包含所有的信息。
要得到这个结果,还需要依赖 API 累计访问的量,即状态。
这个计算模式是将数据输入算子中,用来进行各种复杂的计算并输出数据。这个过程中算子会去访问之前存储在里面的状态。另外一方面,它还会把现在的数据对状态的影响实时更新,如果输入 200 条数据,最后输出就是 200 条结果。
3.2.3 有状态计算的场景
有状态计算用到了以下4个场景:
- 去重:比如上游的系统数据可能会有重复,落到下游系统时希望把重复的数据都去掉。去重需要先了解哪些数据来过,哪些数据还没有来,也就是把所有的主键都记录下来,当一条数据到来后,能够看到在主键当中是否存在。
- 窗口计算:比如统计每分钟
Nginx
日志API
被访问了多少次。窗口是一分钟计算一次,在窗口触发前,如08:00 ~ 08:01
这个窗口,前59
秒的数据来了需要先放入内存,即需要把这个窗口之内的数据先保留下来,等到8:01
时一分钟后,再将整个窗口内触发的数据输出。未触发的窗口数据也是一种状态。 - 机器学习/深度学习:如训练的模型以及当前模型的参数也是一种状态,机器学习可能每次都用有一个数据集,需要在数据集上进行学习,对模型进行一个反馈。
- 访问历史数据:比如与昨天的数据进行对比,需要访问一些历史数据。如果每次从外部去读,对资源的消耗可能比较大,所以也希望把这些历史数据也放入状态中做对比。
04 状态的分类
4.1 Managed State & Raw State
从Flink
是否接管角度可以分为:
- ManagedState(托管状态)
- RawState(原始状态)
类型 | Managed State | Raw State |
---|---|---|
状态管理方式 |
Flink Runtime 管理(自动存储,自动恢复、内存管理上的优化)
|
用户自己管理(需要自己序列化 ) |
状态数据结构 |
已知的数据结构(value,list,map …)
|
字节数组(byte[] )
|
推荐使用场景 | 大多数情况下均可使用 |
自定义Operator 时可使用
|
两者的区别如下:
- 从状态管理方式的方式来说,
Managed State
由Flink Runtime
管理,自动存储,自动恢复,在内存管理上有优化;而Raw State
需要用户自己管理,需要自己序列化,Flink
不知道State
中存入的数据是什么结构,只有用户自己知道,需要最终序列化为可存储的数据结构。 - 从状态数据结构来说,
Managed State
支持已知的数据结构,如Value
、List
、Map
等。而Raw State
只支持字节数组 ,所有状态都要转换为二进制字节数组才可以。 - 从推荐使用场景来说,
Managed State
大多数情况下均可使用,而Raw State
是当Managed State
不够用时,比如需要自定义Operator
时,才会使用Raw State
。
在实际生产中,都只推荐使用ManagedState
。
4.2 Keyed State & Operator State
Managed State 分为两种:Keyed State
和Operator State
(Raw State
都是Operator State
)
类型 | Keyed State | Operator State |
---|---|---|
算子 |
只能用在KeyedStream 上的算子中
|
可以用于所有算子(常用于source ,例如FlinkKafakaConsumer )
|
对应State
|
每个key 对应一个State (一个Operator 实例处理多个key ,访问相应的多个State )
|
一个Operator 实例对应一个State
|
并发改变 | 并发改变,State随着Key在实例间迁移 | 并发改变时有多重重新分配方式可选(均匀分配、合并后每个得到全量) |
访问方式 |
通过RuntimeContext 访问(Rich Function )
|
实现CheckpointedFunction 或ListCheckpointed 接口
|
支持结构 |
支持的数据结构(ValueState 、ListState 、ReducingState 、AggregatingState 、MapState )
|
支持的数据结构(ListState )
|
4.2.1 Keyed State
在Flink Stream
模型中,Datastream
经过 keyBy
的操作可以变为KeyedStream
。
Keyed State
是基于KeyedStream
上的状态。这个状态是跟特定的key
绑定的,对KeyedStream
流上的每一个key
,都对应一个state
,如stream.keyBy(…)
KeyBy
之后的State
,可以理解为分区过的State
,每个并行keyed Operator
的每个实例的每个key
都有一个Keyed State
,即<parallel-operator-instance,key>
就是一个唯一的状态,由于每个key
属于一个keyed Operator
的并行实例,因此我们将其简单的理解为<operator,key>
。
4.2.2 Operator State
这里的fromElements
会调用FromElementsFunction
的类,其中就使用了类型为list state
的operator state
Operator State
又称为 non-keyed state
,与Key
无关的State
,每一个 operator state
都仅与一个operator
的实例绑定。
Operator State
可以用于所有算子,但一般常用于 Source
。
05 State存储结构
前面说过有状态计算其实就是需要考虑历史数据,而历史数据需要搞个地方存储起来,Flink
为了方便不同分类的State
的存储和管理,提供了如下的API
/数据结构来存储State
!
5.1 State API
Keyed State
通过 RuntimeContext
访问,这需要 Operator
是一个RichFunction
。
保存Keyed state
的数据结构:
ValueState<T>
:即类型为T
的单值状态。这个状态与对应的key
绑定,是最简单的状态了。它可以通过update
方法更新状态值,通过value()
方法获取状态值,如求按用户id
统计用户交易总额ListState<T>
:即key
上的状态值为一个列表。可以通过add
方法往列表中附加值;也可以通过get()
方法返回一个Iterable<T>
来遍历状态值,如统计按用户id
统计用户经常登录的Ip
ReducingState<T>
:这种状态通过用户传入的reduceFunction
,每次调用add
方法添加值的时候,会调用reduceFunction
,最后合并到一个单一的状态值MapState<UK, UV>
:即状态值为一个map
。用户通过put
或putAll
方法添加元素
需要注意的是,以上所述的State
对象,仅仅用于与状态进行交互(更新、删除、清空等),而真正的状态值,有可能是存在内存、磁盘、或者其他分布式存储系统中。相当于我们只是持有了这个状态的句柄Operator State
:需要自己实现CheckpointedFunction
或ListCheckpointed
接口。保存Operator state
的数据结构:(ListState<T>
,BroadcastState<K,V>
)
举例来说,Flink
中的FlinkKafkaConsumer
,就使用了operator state
,它会在每个connector
实例中,保存该实例中消费topic
的所有(partition, offset
)映射。
06 State 案例
6.1 Keyed State案例
参考:https://ci.apache.org/projects/flink/flink-docs-stable/dev/stream/state/state.html#using-managed-keyed-state
下图就 word count
的 sum
所使用的StreamGroupedReduce
类为例讲解了如何在代码中使用 keyed state
:
需求:使用KeyState
中的ValueState
获取数据中的最大值(实际中直接使用maxBy
即可)
编码步骤:
//-1.定义一个状态用来存放最大值
private transient ValueState<Long> maxValueState;//-2.创建一个状态描述符对象
ValueStateDescriptor descriptor = new ValueStateDescriptor("maxValueState", Long.class);//-3.根据状态描述符获取State
maxValueState = getRuntimeContext().getState(maxValueStateDescriptor);//-4.使用State
Long historyValue = maxValueState.value();
//判断当前值和历史值谁大
if (historyValue == null || currentValue > historyValue) //-5.更新状态
maxValueState.update(currentValue);
示例代码:
/*** 使用KeyState中的ValueState获取流数据中的最大值(实际中直接使用maxBy即可)** @author : YangLinWei* @createTime: 2022/3/8 12:13 上午*/
public class KeyedState {public static void main(String[] args) throws Exception {//1.envStreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();env.setParallelism(1);//方便观察//2.SourceDataStreamSource<Tuple2<String, Long>> tupleDS = env.fromElements(Tuple2.of("北京", 1L),Tuple2.of("上海", 2L),Tuple2.of("北京", 6L),Tuple2.of("上海", 8L),Tuple2.of("北京", 3L),Tuple2.of("上海", 4L));//3.Transformation//使用KeyState中的ValueState获取流数据中的最大值(实际中直接使用maxBy即可)//实现方式1:直接使用maxBy--开发中使用该方式即可//min只会求出最小的那个字段,其他的字段不管//minBy会求出最小的那个字段和对应的其他的字段//max只会求出最大的那个字段,其他的字段不管//maxBy会求出最大的那个字段和对应的其他的字段SingleOutputStreamOperator<Tuple2<String, Long>> result = tupleDS.keyBy(t -> t.f0).maxBy(1);//实现方式2:使用KeyState中的ValueState---学习测试时使用,或者后续项目中/实际开发中遇到复杂的Flink没有实现的逻辑,才用该方式!SingleOutputStreamOperator<Tuple3<String, Long, Long>> result2 = tupleDS.keyBy(t -> t.f0).map(new RichMapFunction<Tuple2<String, Long>, Tuple3<String, Long, Long>>() {//-1.定义状态用来存储最大值private ValueState<Long> maxValueState = null;@Overridepublic void open(Configuration parameters) throws Exception {//-2.定义状态描述符:描述状态的名称和里面的数据类型ValueStateDescriptor descriptor = new ValueStateDescriptor("maxValueState", Long.class);//-3.根据状态描述符初始化状态maxValueState = getRuntimeContext().getState(descriptor);}@Overridepublic Tuple3<String, Long, Long> map(Tuple2<String, Long> value) throws Exception {//-4.使用State,取出State中的最大值/历史最大值Long historyMaxValue = maxValueState.value();Long currentValue = value.f1;if (historyMaxValue == null || currentValue > historyMaxValue) {//5-更新状态,把当前的作为新的最大值存到状态中maxValueState.update(currentValue);return Tuple3.of(value.f0, currentValue, currentValue);} else {return Tuple3.of(value.f0, currentValue, historyMaxValue);}}});//4.Sink//result.print();result2.print();//5.executeenv.execute();}
}
运行结果:
6.2 Operator State案例
参考:https://ci.apache.org/projects/flink/flink-docs-stable/dev/stream/state/state.html#using-managed-operator-state
下图对 word count
示例中的FromElementsFunction
类进行详解并分享如何在代码中使用operator state
:
需求:使用ListState
存储offset
模拟Kafka
的offset
维护
编码步骤:
//-1.声明一个OperatorState来记录offset
private ListState<Long> offsetState = null;
private Long offset = 0L;//-2.创建状态描述器
ListStateDescriptor<Long> descriptor = new ListStateDescriptor<Long>("offsetState", Long.class);//-3.根据状态描述器获取State
offsetState = context.getOperatorStateStore().getListState(descriptor);//-4.获取State中的值
Iterator<Long> iterator = offsetState.get().iterator();
if (iterator.hasNext()) {//迭代器中有值offset = iterator.next();//取出的值就是offset
}
offset += 1L;
ctx.collect("subTaskId:" + getRuntimeContext().getIndexOfThisSubtask() + ",当前的offset为:" + offset);
if (offset % 5 == 0) {//每隔5条消息,模拟一个异常//-5.保存State到Checkpoint中
offsetState.clear();//清理内存中存储的offset到Checkpoint中//-6.将offset存入State中
offsetState.add(offset);
示例代码:
/*** OperatorState** @author : YangLinWei* @createTime: 2022/3/8 12:17 上午*/
public class OperatorState {public static void main(String[] args) throws Exception {//1.envStreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();env.setParallelism(1);//先直接使用下面的代码设置Checkpoint时间间隔和磁盘路径以及代码遇到异常后的重启策略,下午会学env.enableCheckpointing(1000);//每隔1s执行一次Checkpointenv.setStateBackend(new FsStateBackend("file:///D:/ckp"));env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);//固定延迟重启策略: 程序出现异常的时候,重启2次,每次延迟3秒钟重启,超过2次,程序退出env.setRestartStrategy(RestartStrategies.fixedDelayRestart(2, 3000));//2.SourceDataStreamSource<String> sourceData = env.addSource(new MyKafkaSource());//3.Transformation//4.SinksourceData.print();//5.executeenv.execute();}/*** MyKafkaSource就是模拟的FlinkKafkaConsumer并维护offset*/public static class MyKafkaSource extends RichParallelSourceFunction<String> implements CheckpointedFunction {//-1.声明一个OperatorState来记录offsetprivate ListState<Long> offsetState = null;private Long offset = 0L;private boolean flag = true;@Overridepublic void initializeState(FunctionInitializationContext context) throws Exception {//-2.创建状态描述器ListStateDescriptor descriptor = new ListStateDescriptor("offsetState", Long.class);//-3.根据状态描述器初始化状态offsetState = context.getOperatorStateStore().getListState(descriptor);}@Overridepublic void run(SourceContext<String> ctx) throws Exception {//-4.获取并使用State中的值Iterator<Long> iterator = offsetState.get().iterator();if (iterator.hasNext()) {offset = iterator.next();}while (flag) {offset += 1;int id = getRuntimeContext().getIndexOfThisSubtask();ctx.collect("分区:" + id + "消费到的offset位置为:" + offset);//1 2 3 4 5 6//Thread.sleep(1000);TimeUnit.SECONDS.sleep(2);if (offset % 5 == 0) {System.out.println("程序遇到异常了.....");throw new Exception("程序遇到异常了.....");}}}@Overridepublic void cancel() {flag = false;}/*** 下面的snapshotState方法会按照固定的时间间隔将State信息存储到Checkpoint/磁盘中,也就是在磁盘做快照!*/@Overridepublic void snapshotState(FunctionSnapshotContext context) throws Exception {//-5.保存State到Checkpoint中offsetState.clear();//清理内存中存储的offset到Checkpoint中//-6.将offset存入State中offsetState.add(offset);}}
}
运行结果:
07 文末
本文主要讲解了Flink
高级API
里面的状态管理,谢谢大家的阅读,本文完!
Flink教程(13)- Flink高级API(状态管理)相关推荐
- Flink教程(13) Keyed State状态管理之ValueState的使用 温差报警
Keyed State状态管理之ValueState的使用 温差报警 系列文章 一.ValueState的方法 二.实验案例 1. 温度Bean 2. 将字符串映射成SensorRecord对象 3. ...
- 【Flink】Flink 1.13 Flink SQL 新特性 性能优化 时区 时间 纠正
文章目录 1.概述 2.window TVF 2.2 GROUPING 2.3 window TopN 2.4 RollUP 2.5 优化 3.时间 3.1 时间函数纠正 3.2 时间类型的使用 3. ...
- flink教程-聊聊 flink 1.11 中新的水印策略
文章目录 背景 新的水印生成接口 内置水印生成策略 固定延迟生成水印 单调递增生成水印 event时间的获取 处理空闲数据源 背景 在flink 1.11之前的版本中,提供了两种生成水印(Waterm ...
- Flink教程(14)- Flink高级API(容错机制)
文章目录 01 引言 02 Checkpoint 2.1 Checkpoint VS State 2.2 Checkpoint 执行流程 2.2.1 简单流程 2.2.2 复杂流程 2.3 State ...
- Flink教程(11)- Flink高级API(Window)
文章目录 01 引言 02 Window 2.1 为什么需要Window? 2.2 Window分类 2.2.1 按照time和count分类 2.2.2 按照slide和size分类 2.2.3 总 ...
- Flink教程(20)- Flink高级特性(双流Join)
文章目录 01 引言 02 双流join介绍 03 Window Join 3.1 Tumbling Window Join 3.2 Sliding Window Join 3.3 Session W ...
- Flink教程(29)- Flink内存管理
文章目录 01 引言 02 Flink内存管理 2.1 Flink内存划分 2.2 Flink堆外内存 2.3 序列化与反序列化 2.4 操纵二进制数据 2.5 注意 03 文末 01 引言 在前面的 ...
- Flink教程(22)- Flink高级特性(异步IO)
文章目录 01 引言 02 异步IO 2.1 异步IO介绍 2.2 使用Aysnc I/O的前提条件 2.3 Async I/O API 03 案例演示 04 原理深入 4.1 AsyncDataSt ...
- Flink教程(25)- Flink高级特性(FlinkSQL整合Hive)
文章目录 01 引言 02 FlinkSQL 整合Hive 2.1 介绍 2.2 集成Hive的基本方式 2.3 准备工作 2.4 SQL CLI 2.5 代码演示 03 文末 01 引言 在前面的博 ...
最新文章
- PyTorch LSTM,batch_first=True对初始化h0和c0的影响
- 大数据的“近因偏差”烦恼
- 类风湿性关节炎患者腕关节的多普勒超声积分与OMERACT RAMRIS骨髓水肿和滑膜相关...
- 解决org.apache.hadoop.io.nativeio.NativeIOException: 当文件已存在时,无法创建该文件。
- grasp设计模式应用场景_grasp设计模式笔记回顾
- ps4看b站 f怎么调html5,b站html5,b站怎么切换到HTML5版播放器?
- Linux下C/C++程序编译链接加载过程中的常见问题及解决方法
- oracle ora32771,Oracle的文件号、相对文件号及其他(续)
- VMware vSphere “I moved it” or “I copied it” – What’s the difference?
- Windows远程桌面开发之九-虚拟显示器(Windows 10 Indirect Display 虚拟显示器驱动开发)
- 大数据收集系统架构图
- veu项目实践详细笔记(一)
- 我梦见了画,然后画下了梦
- phabricator 结合 arcanist 使用
- 如何用木板做桥_用木板做桥 工具跟做家具的一样 大小跟办公桌差不多大 能承重 参加比赛 主要是承重 给个设计方案...
- JAVA 日期推算---算法
- 香港公司--离岸帐户现金
- React + webpack 开发单页面应用简明中文文档教程(一)一些基础概念
- 架构师接龙 岑文初VS. 杨海朝_系统架构
- C语言 写一个函数,输入一个4位数字,要求输出这4个数字字符,但每两个数字之间空一个空格。如输入1990,应该输出“1 9 9 0”