目录

扩展阅读  异步IO

介绍

异步IO操作的需求

使用Aysnc I/O的前提条件

Async I/O API

案例演示

扩展阅读 原理深入

AsyncDataStream

消息的顺序性


扩展阅读  异步IO

介绍

异步IO操作的需求

Apache Flink 1.12 Documentation: Asynchronous I/O for External Data Access

Async I/O 是阿里巴巴贡献给社区的一个呼声非常高的特性,于1.2版本引入。主要目的是为了解决与外部系统交互时网络延迟成为了系统瓶颈的问题。

流计算系统中经常需要与外部系统进行交互,我们通常的做法如向数据库发送用户a的查询请求,然后等待结果返回,在这之前,我们的程序无法发送用户b的查询请求。这是一种同步访问方式,如下图所示

  1. 左图所示:通常实现方式是向数据库发送用户a的查询请求(例如在MapFunction中),然后等待结果返回,在这之前,我们无法发送用户b的查询请求,这是一种同步访问的模式,图中棕色的长条标识等待时间,可以发现网络等待时间极大的阻碍了吞吐和延迟
  2. 右图所示:为了解决同步访问的问题,异步模式可以并发的处理多个请求和回复,可以连续的向数据库发送用户a、b、c、d等的请求,与此同时,哪个请求的回复先返回了就处理哪个回复,从而连续的请求之间不需要阻塞等待,这也正是Async I/O的实现原理。

使用Aysnc I/O的前提条件

  1. 数据库(或key/value存储系统)提供支持异步请求的client。(如java的vertx)
  2. 没有异步请求客户端的话也可以将同步客户端丢到线程池中执行作为异步客户端


Async I/O API

Async I/O API允许用户在数据流中使用异步客户端访问外部存储,该API处理与数据流的集成,以及消息顺序性(Order),事件时间(EventTime),一致性(容错)等脏活累活,用户只专注于业务

如果目标数据库中有异步客户端,则三步即可实现异步流式转换操作(针对该数据库的异步):

  • 实现用来分发请求的AsyncFunction,用来向数据库发送异步请求并设置回调
  • 获取操作结果的callback,并将它提交给ResultFuture
  • 将异步I/O操作应用于DataStream

案例演示

两种方式实现Flink异步IO查询Mysql_优优我心的博客-CSDN博客

需求:

使用异步IO实现从MySQL中读取数据

数据准备:


DROP TABLE IF EXISTS `t_category`;CREATE TABLE `t_category` (`id` int(11) NOT NULL,`name` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Records of t_category-- ----------------------------INSERT INTO `t_category` VALUES ('1', '手机');INSERT INTO `t_category` VALUES ('2', '电脑');INSERT INTO `t_category` VALUES ('3', '服装');INSERT INTO `t_category` VALUES ('4', '化妆品');INSERT INTO `t_category` VALUES ('5', '食品');

代码演示-异步IO读取MySQL

package cn.i.extend;import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.jdbc.JDBCClient;
import io.vertx.ext.sql.SQLClient;
import io.vertx.ext.sql.SQLConnection;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.AsyncDataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.async.ResultFuture;
import org.apache.flink.streaming.api.functions.async.RichAsyncFunction;
import org.apache.flink.streaming.api.functions.source.RichSourceFunction;import java.sql.*;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;/*** 使用异步io的先决条件* 1.数据库(或key/value存储)提供支持异步请求的client。* 2.没有异步请求客户端的话也可以将同步客户端丢到线程池中执行作为异步客户端。*/
public class ASyncIODemo {public static void main(String[] args) throws Exception {//1.envStreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();//2.Source//DataStreamSource[1,2,3,4,5]DataStreamSource<CategoryInfo> categoryDS = env.addSource(new RichSourceFunction<CategoryInfo>() {private Boolean flag = true;@Overridepublic void run(SourceContext<CategoryInfo> ctx) throws Exception {Integer[] ids = {1, 2, 3, 4, 5};for (Integer id : ids) {ctx.collect(new CategoryInfo(id, null));}}@Overridepublic void cancel() {this.flag = false;}});//3.Transformation//方式一:Java-vertx中提供的异步client实现异步IO//unorderedWait无序等待SingleOutputStreamOperator<CategoryInfo> result1 = AsyncDataStream.unorderedWait(categoryDS, new ASyncIOFunction1(), 1000, TimeUnit.SECONDS, 10);//方式二:MySQL中同步client+线程池模拟异步IO//unorderedWait无序等待SingleOutputStreamOperator<CategoryInfo> result2 = AsyncDataStream.unorderedWait(categoryDS, new ASyncIOFunction2(), 1000, TimeUnit.SECONDS, 10);//4.Sinkresult1.print("方式一:Java-vertx中提供的异步client实现异步IO \n");result2.print("方式二:MySQL中同步client+线程池模拟异步IO \n");//5.executeenv.execute();}
}@Data
@NoArgsConstructor
@AllArgsConstructor
class CategoryInfo {private Integer id;private String name;
}class MysqlSyncClient {private static transient Connection connection;private static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";private static final String URL = "jdbc:mysql://localhost:3306/bigdata";private static final String USER = "root";private static final String PASSWORD = "root";static {init();}private static void init() {try {Class.forName(JDBC_DRIVER);} catch (ClassNotFoundException e) {System.out.println("Driver not found!" + e.getMessage());}try {connection = DriverManager.getConnection(URL, USER, PASSWORD);} catch (SQLException e) {System.out.println("init connection failed!" + e.getMessage());}}public void close() {try {if (connection != null) {connection.close();}} catch (SQLException e) {System.out.println("close connection failed!" + e.getMessage());}}public CategoryInfo query(CategoryInfo category) {try {String sql = "select id,name from t_category where id = "+ category.getId();Statement statement = connection.createStatement();ResultSet rs = statement.executeQuery(sql);if (rs != null && rs.next()) {category.setName(rs.getString("name"));}} catch (SQLException e) {System.out.println("query failed!" + e.getMessage());}return category;}
}/*** 方式一:Java-vertx中提供的异步client实现异步IO*/
class ASyncIOFunction1 extends RichAsyncFunction<CategoryInfo, CategoryInfo> {private transient SQLClient mySQLClient;@Overridepublic void open(Configuration parameters) throws Exception {JsonObject mySQLClientConfig = new JsonObject();mySQLClientConfig.put("driver_class", "com.mysql.jdbc.Driver").put("url", "jdbc:mysql://localhost:3306/bigdata").put("user", "root").put("password", "root").put("max_pool_size", 20);VertxOptions options = new VertxOptions();options.setEventLoopPoolSize(10);options.setWorkerPoolSize(20);Vertx vertx = Vertx.vertx(options);//根据上面的配置参数获取异步请求客户端mySQLClient = JDBCClient.createNonShared(vertx, mySQLClientConfig);}//使用异步客户端发送异步请求@Overridepublic void asyncInvoke(CategoryInfo input, ResultFuture<CategoryInfo> resultFuture) throws Exception {mySQLClient.getConnection(new Handler<AsyncResult<SQLConnection>>() {@Overridepublic void handle(AsyncResult<SQLConnection> sqlConnectionAsyncResult) {if (sqlConnectionAsyncResult.failed()) {return;}SQLConnection connection = sqlConnectionAsyncResult.result();connection.query("select id,name from t_category where id = " +input.getId(), new Handler<AsyncResult<io.vertx.ext.sql.ResultSet>>() {@Overridepublic void handle(AsyncResult<io.vertx.ext.sql.ResultSet> resultSetAsyncResult) {if (resultSetAsyncResult.succeeded()) {List<JsonObject> rows = resultSetAsyncResult.result().getRows();for (JsonObject jsonObject : rows) {CategoryInfo categoryInfo = new CategoryInfo(jsonObject.getInteger("id"), jsonObject.getString("name"));resultFuture.complete(Collections.singletonList(categoryInfo));}}}});}});}@Overridepublic void close() throws Exception {mySQLClient.close();}@Overridepublic void timeout(CategoryInfo input, ResultFuture<CategoryInfo> resultFuture) throws Exception {System.out.println("async call time out!");input.setName("未知");resultFuture.complete(Collections.singleton(input));}
}/*** 方式二:同步调用+线程池模拟异步IO*/
class ASyncIOFunction2 extends RichAsyncFunction<CategoryInfo, CategoryInfo> {private transient MysqlSyncClient client;private ExecutorService executorService;//线程池@Overridepublic void open(Configuration parameters) throws Exception {super.open(parameters);client = new MysqlSyncClient();executorService = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());}//异步发送请求@Overridepublic void asyncInvoke(CategoryInfo input, ResultFuture<CategoryInfo> resultFuture) throws Exception {executorService.execute(new Runnable() {@Overridepublic void run() {resultFuture.complete(Collections.singletonList((CategoryInfo) client.query(input)));}});}@Overridepublic void close() throws Exception {}@Overridepublic void timeout(CategoryInfo input, ResultFuture<CategoryInfo> resultFuture) throws Exception {System.out.println("async call time out!");input.setName("未知");resultFuture.complete(Collections.singleton(input));}
}

异步IO读取Redis数据-没必要!!!

package cn.it.extend;import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.redis.RedisClient;
import io.vertx.redis.RedisOptions;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.AsyncDataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.async.ResultFuture;
import org.apache.flink.streaming.api.functions.async.RichAsyncFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
使用异步IO访问redis
hset AsyncReadRedis beijing 1
hset AsyncReadRedis shanghai 2
hset AsyncReadRedis guangzhou 3
hset AsyncReadRedis shenzhen 4
hset AsyncReadRedis hangzhou 5
hset AsyncReadRedis wuhan 6
hset AsyncReadRedis chengdu 7
hset AsyncReadRedis tianjin 8
hset AsyncReadRedis chongqing 9city.txt
1,beijing
2,shanghai
3,guangzhou
4,shenzhen
5,hangzhou
6,wuhan
7,chengdu
8,tianjin
9,chongqing*/
public class AsyncIODemo_Redis {public static void main(String[] args) throws Exception {StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);DataStreamSource<String> lines = env.readTextFile("data/input/city.txt");SingleOutputStreamOperator<String> result1 = AsyncDataStream.orderedWait(lines, new AsyncRedis(), 10, TimeUnit.SECONDS, 1);SingleOutputStreamOperator<String> result2 = AsyncDataStream.orderedWait(lines, new AsyncRedisByVertx(), 10, TimeUnit.SECONDS, 1);result1.print().setParallelism(1);result2.print().setParallelism(1);env.execute();}
}
/*** 使用异步的方式读取redis的数据*/
class AsyncRedis extends RichAsyncFunction<String, String> {//定义redis的连接池对象private JedisPoolConfig config = null;private static String ADDR = "localhost";private static int PORT = 6379;//等待可用连接的最大时间,单位是毫秒,默认是-1,表示永不超时,如果超过等待时间,则会抛出异常private static int TIMEOUT = 10000;//定义redis的连接池实例private JedisPool jedisPool = null;//定义连接池的核心对象private Jedis jedis = null;//初始化redis的连接@Overridepublic void open(Configuration parameters) throws Exception {super.open(parameters);//定义连接池对象属性配置config = new JedisPoolConfig();//初始化连接池对象jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT);//实例化连接对象(获取一个可用的连接)jedis = jedisPool.getResource();}@Overridepublic void close() throws Exception {super.close();if(jedis.isConnected()){jedis.close();}}//异步调用redis@Overridepublic void asyncInvoke(String input, ResultFuture<String> resultFuture) throws Exception {System.out.println("input:"+input);//发起一个异步请求,返回结果CompletableFuture.supplyAsync(new Supplier<String>() {@Overridepublic String get() {String[] arrayData = input.split(",");String name = arrayData[1];String value = jedis.hget("AsyncReadRedis", name);System.out.println("output:"+value);return  value;}}).thenAccept((String dbResult)->{//设置请求完成时的回调,将结果返回resultFuture.complete(Collections.singleton(dbResult));});}//连接超时的时候调用的方法,一般在该方法中输出连接超时的错误日志,如果不重新该方法,连接超时后会抛出异常@Overridepublic void timeout(String input, ResultFuture<String> resultFuture) throws Exception {System.out.println("redis connect timeout!");}
}
/*** 使用高性能异步组件vertx实现类似于连接池的功能,效率比连接池要高* 1)在java版本中可以直接使用* 2)如果在scala版本中使用的话,需要scala的版本是2.12+*/
class AsyncRedisByVertx extends RichAsyncFunction<String,String> {//用transient关键字标记的成员变量不参与序列化过程private transient RedisClient redisClient;//获取连接池的配置对象private JedisPoolConfig config = null;//获取连接池JedisPool jedisPool = null;//获取核心对象Jedis jedis = null;//Redis服务器IPprivate static String ADDR = "localhost";//Redis的端口号private static int PORT = 6379;//访问密码private static String AUTH = "XXXXXX";//等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;private static int TIMEOUT = 10000;private static final Logger logger = LoggerFactory.getLogger(AsyncRedis.class);//初始化连接@Overridepublic void open(Configuration parameters) throws Exception {super.open(parameters);config = new JedisPoolConfig();jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT);jedis = jedisPool.getResource();RedisOptions config = new RedisOptions();config.setHost(ADDR);config.setPort(PORT);VertxOptions vo = new VertxOptions();vo.setEventLoopPoolSize(10);vo.setWorkerPoolSize(20);Vertx vertx = Vertx.vertx(vo);redisClient = RedisClient.create(vertx, config);}//数据异步调用@Overridepublic void asyncInvoke(String input, ResultFuture<String> resultFuture) throws Exception {System.out.println("input:"+input);String[] split = input.split(",");String name = split[1];// 发起一个异步请求redisClient.hget("AsyncReadRedis", name, res->{if(res.succeeded()){String result = res.result();if(result== null){resultFuture.complete(null);return;}else {// 设置请求完成时的回调: 将结果传递给 collectorresultFuture.complete(Collections.singleton(result));}}else if(res.failed()) {resultFuture.complete(null);return;}});}@Overridepublic void timeout(String input, ResultFuture resultFuture) throws Exception {}@Overridepublic void close() throws Exception {super.close();if (redisClient != null) {redisClient.close(null);}}
}

扩展阅读 原理深入

AsyncDataStream

AsyncDataStream是一个工具类,用于将AsyncFunction应用于DataStream,AsyncFunction发出的并发请求都是无序的,该顺序基于哪个请求先完成,为了控制结果记录的发出顺序,flink提供了两种模式,分别对应AsyncDataStream的两个静态方法,OrderedWaitunorderedWait

  • orderedWait(有序):消息的发送顺序与接收到的顺序相同(包括 watermark ),也就是先进先出。
  • unorderWait(无序):
    • 在ProcessingTime中,完全无序,即哪个请求先返回结果就先发送(最低延迟和最低消耗)。
    • 在EventTime中,以watermark为边界,介于两个watermark之间的消息可以乱序,但是watermark和消息之间不能乱序,这样既认为在无序中又引入了有序,这样就有了与有序一样的开销。

AsyncDataStream.(un)orderedWait 的主要工作就是创建了一个 AsyncWaitOperator。AsyncWaitOperator 是支持异步 IO 访问的算子实现,该算子会运行 AsyncFunction 并处理异步返回的结果,其内部原理如下图所示。

如图所示,AsyncWaitOperator 主要由两部分组成:

  • StreamElementQueue
  • Emitter

StreamElementQueue 是一个 Promise 队列,所谓 Promise 是一种异步抽象表示将来会有一个值(海底捞排队给你的小票),这个队列是未完成的 Promise 队列,也就是进行中的请求队列。Emitter 是一个单独的线程,负责发送消息(收到的异步回复)给下游。

图中E5表示进入该算子的第五个元素(”Element-5”)

  • 在执行过程中首先会将其包装成一个 “Promise” P5,然后将P5放入队列
  • 最后调用 AsyncFunction 的 ayncInvoke 方法,该方法会向外部服务发起一个异步的请求,并注册回调
  • 该回调会在异步请求成功返回时调用 AsyncCollector.collect 方法将返回的结果交给框架处理。
  • 实际上 AsyncCollector 是一个 Promise ,也就是 P5,在调用 collect 的时候会标记 Promise 为完成状态,并通知 Emitter 线程有完成的消息可以发送了。
  • Emitter 就会从队列中拉取完成的 Promise ,并从 Promise 中取出消息发送给下游。

​​​​​​​消息的顺序性

上文提到 Async I/O 提供了两种输出模式。其实细分有三种模式:

  • 有序
  • ProcessingTime 无序
  • EventTime 无序

Flink 使用队列来实现不同的输出模式,并抽象出一个队列的接口(StreamElementQueue),这种分层设计使得AsyncWaitOperator和Emitter不用关心消息的顺序问题。StreamElementQueue有两种具体实现,分别是 OrderedStreamElementQueue 和UnorderedStreamElementQueue。UnorderedStreamElementQueue 比较有意思,它使用了一套逻辑巧妙地实现完全无序和 EventTime 无序。

  • 有序

有序比较简单,使用一个队列就能实现。所有新进入该算子的元素(包括 watermark),都会包装成 Promise 并按到达顺序放入该队列。如下图所示,尽管P4的结果先返回,但并不会发送,只有 P1 (队首)的结果返回了才会触发 Emitter 拉取队首元素进行发送。

  • ProcessingTime 无序

ProcessingTime 无序也比较简单,因为没有 watermark,不需要协调 watermark 与消息的顺序性,所以使用两个队列就能实现,一个 uncompletedQueue 一个 completedQueue。所有新进入该算子的元素,同样的包装成 Promise 并放入 uncompletedQueue 队列,当uncompletedQueue队列中任意的Promise返回了数据,则将该 Promise 移到 completedQueue 队列中,并通知 Emitter 消费。如下图所示:

  • EventTime 无序

EventTime 无序类似于有序与 ProcessingTime 无序的结合体。因为有 watermark,需要协调 watermark与消息之间的顺序性,所以uncompletedQueue中存放的元素从原先的 Promise 变成了 Promise 集合

  • 如果进入算子的是消息元素,则会包装成 Promise 放入队尾的集合中
  • 如果进入算子的是 watermark,也会包装成 Promise 并放到一个独立的集合中,再将该集合加入到 uncompletedQueue 队尾,最后再创建一个空集合加到 uncompletedQueue 队尾
  • 这样,watermark 就成了消息顺序的边界。
  • 只有处在队首的集合中的 Promise 返回了数据,才能将该 Promise 移到completedQueue
  • 队列中,由 Emitter 消费发往下游。
  • 只有队首集合空了,才能处理第二个集合。

这样就保证了当且仅当某个 watermark 之前所有的消息都已经被发送了,该 watermark 才能被发送。过程如下图所示:

2021年大数据Flink(四十六):扩展阅读 异步IO相关推荐

  1. 2021年大数据Flink(十六):流批一体API Connectors ​​​​​​​​​​​​​​Redis

    目录 Redis API 使用RedisCommand设置数据结构类型时和redis结构对应关系 需求 代码实现 Redis API 通过flink 操作redis 其实我们可以通过传统的redis ...

  2. 2021年大数据HBase(十六):HBase的协处理器(Coprocessor)

    全网最详细的大数据HBase文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 系列历史文章 HBase的协处理器(Coprocessor) 一.起源 二 ...

  3. 2021年大数据ELK(十六):Elasticsearch SQL(职位查询案例)

    全网最详细的大数据ELK文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 职位查询案例 一.查询职位索引库中的一条数据 二.将SQL转换为DSL 三.职 ...

  4. 2021年大数据Flink(十四):流批一体API Connectors JDBC

    目录 Connectors JDBC 代码演示 Connectors JDBC Apache Flink 1.12 Documentation: JDBC Connector 代码演示 package ...

  5. 2021年大数据Flink(十五):流批一体API Connectors ​​​​​​​Kafka

    目录 Kafka pom依赖 参数设置 参数说明 Kafka命令 代码实现-Kafka Consumer 代码实现-Kafka Producer 代码实现-实时ETL Kafka pom依赖 Flin ...

  6. 2021年大数据Flink(十二):流批一体API Transformation

    目录 Transformation 官网API列表 基本操作-略 map flatMap keyBy filter sum reduce 代码演示 合并-拆分 union和connect split. ...

  7. 2021年大数据Flink(十):流处理相关概念

    目录 流处理相关概念 数据的时效性 ​​​​​​​流处理和批处理 ​​​​​​​流批一体API DataStream API 支持批执行模式 API 编程模型 ​​​​​​​流处理相关概念 数据的时效 ...

  8. 2021年大数据Flink(十八):Flink Window操作

    目录 ​​​​​​​Flink-Window操作 为什么需要Window Window的分类 按照time和count分类 ​​​​​​​按照slide和size分类 ​​​​​​​总结 Window ...

  9. 客快物流大数据项目(四十六):Spark操作Kudu dataFrame操作kudu

    Spark操作Kudu dataFrame操作kudu 一.DataFrameApi读取kudu表中的数据 虽然我们可以通过上面显示的KuduContext执行大量操作,但我们还可以直接从默认数据源本 ...

  10. 2021年大数据Flink(十九):案例一 基于时间的滚动和滑动窗口

    目录 案例一 基于时间的滚动和滑动窗口 需求 代码实现 案例一 基于时间的滚动和滑动窗口 需求 nc -lk 9999 有如下数据表示: 信号灯编号和通过该信号灯的车的数量 9,3 9,2 9,7 4 ...

最新文章

  1. 此上下文中不允许函数定义。_深度好文 | 你知道Go中的 context 是怎么实现的吗?...
  2. WebBrowser页面与WinForm交互技巧
  3. 找中位数,找第k小,还存在问题
  4. 触摸屏校准没反应,启动时出现No raw modules loaded.ts_config:No such file or directory错误的解决
  5. Kafka是靠什么机制保持高可靠,高可用的?
  6. Pro*c使用指示变量来处理NULL列值
  7. AJAX中的跨域问题:什么是跨域?如何解决跨域问题?
  8. Android Mac下反编译apk
  9. 梯度下降法的三种形式批量梯度下降法、随机梯度下降以及小批量梯度下降法
  10. Bootstrap 导航条
  11. mysql 表单插入数据_PHP表单数据写入MySQL数据库的代码
  12. 2019年春季学期《软件工程》教学总结
  13. 戴尔电脑开机卡logo无法开机问题及解决办法
  14. Java实验1:个人银行账户管理系统总结
  15. 【报告分享】2021全球自由行报告-中国旅游研究院马蜂窝(附下载)
  16. 用Arduino制作红外线循迹自动机器人
  17. 魔兽世界运营时间线timeLine(2004-2014)
  18. halcon相机标定及畸变矫正
  19. Python代码:检查学号是否合法
  20. 苟延残喘,回光返照:从Cheetah 10K.7到Cheetah NS

热门文章

  1. kali2020进入单模式_蚂蚁集团技术专家山丘:性能优化的常见模式及趋势
  2. Linux下 C语言统计时间差
  3. DateGridView列的输出顺序反了
  4. JAVA如何实现发送短信
  5. bert-as-service使用
  6. Swift与LLVM-Clang原理与示例
  7. 女友问粉丝过万如何庆祝,我发万字长文《保姆级大数据入门篇》感恩粉丝们支持,学姐|学妹|学弟|小白看了就懂
  8. 【注意事项】Markdown遇到的小问题
  9. HarmonyOS shape 的使用
  10. ValueError: invalid literal for int() with base 10: “ ”