PointGet的一生
原文来源: https://tidb.net/blog/d6444c63
一、前言
此前,作为 DBA 觉得能看源码是一件很牛的事情,花了 大半年时间 对 Golang 和 Rust 入了个门( 可能入门都不算 ),并写了个 Rust 小工具: TiHC(TiDB Healthy Check) 有兴趣的小伙伴可以自取。
期间,看过某些模块,如: PD 如何调度 Region ,也只是窥探数据库的局部功能,总有一种 “根本不了解数据库逻辑概念和代码是怎样关联的!” 的感觉。因此借着 “点查” 避开大量复杂优化器代码的机会,尽己所能的串联一下点查在 TiDB 和 TiKV 间的执行流程。
此后,越发觉得 “产研” 及 “交付” 价值的不同,比如:不能说看代码就很牛,在文档丰富、产品成熟的前提下, 学习文档才是较快速、较全面、较有价值的方法 ,更多思考见总结部分。
Tips
1.为防止后续代码重构影响,本次对 TiDB v5.3.0 tidb 和 tikv 进行断点调试。
2.为便于读者理解,在本文首次出现的函数或方法均会给出 Github 锚点链接,重复出现将不再标记 URL 链接。
二、摘要
本文主要内容分布在 “三、点查流程” ,所谓点查,即:执行计划的一种算子,如图所示,详见官网介绍 Point_Get 。 “3.1 TiDB 部分” 介绍了点查 SQL 在 TiDB 内部流转过程。 “3.2 TiKV 部分” 介绍了点查经由 TiDB 处理并请求给 TiKV 后,如何在 TiKV 内部流转、处理、返回的过程。 “3.3 总结” 简要介绍了 tidb 点查快的原因。
每个模块的 “Model Tips” 均会说明该模块在 TIDB 各所属组件中的作用。并且尽力采用分层描述的方式说明,所谓分层,比如:Executor 调用了 PD Client,那么只在 Executor 部分描述那个函数触发调用 PD Client 的逻辑,而不在这一层详述 PD Client 如何工作。
最后,在 “四、学习总结” 中,介绍了 个人对 DBA 的看代码行为的价值 的观点。
三、点查流程
3.1 TiDB
下述 3.1 部分均为点查在 TiDB 组件中涉及处理流程介绍。大致如下图:
1. 首先 ,客户端连接进入 MySQL Protocol Layer 接入 TiDB ,在获取 Token后,传送 SQL 执行;
2. 其次 ,SQL 处理进入 Parser 层,解析成 AST(抽象语法树);
3. 再次 ,SQL 处理进入 Compile 层,选出 TSO 并将 AST 编译成执行计划;
4. 然后 ,SQL 处理进入 Executor 层 (可 “Batch 处理” 的火山模型) ,将执行计划 Open、Next、Close 完成 TiKV 数据获取;
5. 最后 ,SQL 处理回到 MySQL Protocol Layer 调用 writeChunks 将 MemBuffer 中数据导出成客户端所需形式返回;
通过追溯 func (cc *clientConn) handleQuery(...) --> func (cc *clientConn) handleStmt(...) --> func (tc *TiDBContext) ExecuteStmt(...) 是串联点查,从解析、编译、执行、协议回显的方法,看懂此函数对于理解全文至关重要。
3.1.1 SQL Protol Deal
Model Tips:
TiDB SQL Protol 处理层仅是 TiDB 为实现 MySQL Protocol 兼容,所做的代码处理。 主要功能包含:监听客户端请求、分发不同 SQL 处理、调用 Parser、Compiler、Executor 完成 SQL 处理,回写结果集等功能。
1. 首先 ,启动 Server 时在 Struct clientConn 内部封装了 go 原生包 net 进行 TCP 通信,并在 for 循环起两个 Listener goroutine 处理客户端发过来的消息。
// Run runs the server.
func (s *Server) Run() error {go s.startNetworkListener(s.listener, false, errChan)go s.startNetworkListener(s.socket, true, errChan)......
}func (s *Server) startNetworkListener(listener net.Listener, isUnixSocket bool, errChan chan error) {for {conn, err := listener.Accept()......go s.onConn(clientConn)}
}
2. 其次 ,在 Dispatch 方法中会首先获取 token ,用于限制单个 TiDB Instance 可处理的同时执行请求的 session 个数,详情见: token-limit ;
3. 再次 ,在 Struct clientConn 实现了 Run、readPacket、writePacket、handshake、openSession、handleQuery、handleStmt 等等方法,用于实现 MySQL Client/Server Protocol 。本例中,从 MySQL Client 发来的点查 SQL 通过 dispatch 方法 遵照 MySQL Protocol 进入 handleQuery 分支处理;
4. 然后 ,handleQuery 会循环处理 Session 中每一个 SQL,在 handleStmt 中对每一条 SQL 进行解析、编译、执行;
5. 最后 ,在 handleStmt 中调用 writeResultset 方法,触发组织好的 Executor 的 Next() 方法从 TiKV 获取数据;
3.1.2 SQL Parser Deal
Model Tips: SQL Parser 处理层通过封装 YACC 实现 MySQL 的词法解析,将 SQL 转化为 AST。
1. 首先 ,在 handleQuery 中,调用 func (s *session) Parse(...) 方法实现 AST 的转化与返回;
2. 其次 ,深入了解会发现,该 Parse 方法为调用 func (s *session) ParseSQL(...) 函数实现真正的词法解析动作,并记录解析 SQL 是否成功及解析耗费的时间。
3. 最后 ,进入 func (s *session) Parse(...) 首先封装一个 sync.Pool 作为 parserPool 减轻 GC struct 的压力,并 Copy AST 结果返回给上层调用,再深层 DEBUG 将会进入 Parser 模块。
func (s *session) ParseSQL(ctx context.Context, sql string, params ...parser.ParseParam) ([]ast.StmtNode, []error, error) {......p := parserPool.Get().(*parser.Parser)defer parserPool.Put(p)p.SetSQLMode(s.sessionVars.SQLMode)p.SetParserConfig(s.sessionVars.BuildParserConfig())tmp, warn, err := p.ParseSQL(sql, params...)res := make([]ast.StmtNode, len(tmp))copy(res, tmp)return res, warn, err
}
3.1.3 SQL Compile Deal
Model Tips:
SQL Compiler 处理层完成 SQL 语意检查(Preprocess)、编译执行计划(Logical Optimize、Physical Optimize) 工作,将 SQL 编译成可执行的物理执行计划。
1. 首先 ,从 MySQL Protocol Layer 串联起 “解析”、“执行” 操作,并在 func (s *session) ExecuteStmt(...) 中调用 func (c *Compiler) Compile(...) 进行真正的编译处理。
2. 其次 ,在 Compile 内部调用 func Preprocess(...) ,进行 Preprocess 完成前置检查,如:语义检查。具体实现流程为,通过 AST 的 Accept 方法 , 构造一个 Vistor 实现对 AST 的遍历。 每个 Visitor 接口包含 Enter、Leave 方法, 并在 Enter 或 Leave 时 ,依据 SQL 类型进行判断。本例 point get 会跳到 func (n *SelectStmt) Accept(v Visitor) 中,不断分支处理完成遍历。详情参考 TiDB 源码阅读之 Compiler --> 进度 10min 左右 。
3. 最后 ,进入 Optimizer 处理,本例中因为是点查会越过大量优化器处理过程,直接进入 func TryFastPlan(...) 进行简单的 “权限检查” 及 “数据库名检查”。
func TryFastPlan(ctx sessionctx.Context, node ast.Node) (p Plan) {......case *ast.SelectStmt:if fp := tryPointGetPlan(ctx, x, isForUpdateReadSelectLock(x.LockInfo)); fp != nil {if checkFastPlanPrivilege(ctx, fp.dbName, fp.TblInfo.Name.L, mysql.SelectPriv) != nil {return nil}if tidbutil.IsMemDB(fp.dbName) {return nil}if fp.IsTableDual {return}p = fpreturn}}return nil
}
3.1.4 SQL Executor Deal
Model Tips:
SQL Executor 通过接收经过 Optimizer 的 Plan,并构造、执行火山模型获取执行结果,火山模型详见 知乎 -- SQL 优化之火山模型 。
1. 首先 ,在 func (s *session) ExecuteStmt(...) 中调用 func (a *ExecStmt) Exec(...) ,调用 Open() 方法从而完成 Executor 变异版火山模型的构造,最后返回一个 RecordSet。
2. 其次 ,ExecuteStmt 返回的 RecordSet 包含了 Executor 需要的所有信息,此时可以把 RecordSet 当成返回的结果集,但该执行流从未触发过 Next() 方法,即:没有真正的获取数据。
3. 最后 ,实际上 Next() 触发由 的 writeResultset 触发,详细流程如下: func (cc *clientConn) handleStmt(...) --> func (cc *clientConn) writeResultset(...) --> func (cc *clientConn) writeChunks(...) --> func (trs *tidbResultSet) Next(...) --> func (a *recordSet) Next(...) --> func Next(...) --> func (e *PointGetExecutor) Next(...) --> func (e *PointGetExecutor) Next(...) --> func (e *PointGetExecutor) getAndLock(...) --> func (e *PointGetExecutor) get(...) 层层调用,直至 Executor 完成所有数据的获取。
4. 另外 ,值得一提的是在 func (a *ExecStmt) Exec(...) --> Build Executor 时,因为点查使用“主键”或“唯一索引”标定一行, 不存在重复数据读 ,所以在 autoCommit
情况下直接取 MaxUint64
作为事务的 StartTS(该事务只有一个点查),即:无穷大 +∞。同时,还会赋予 PriorityHigh
优先级进行处理,详情见 force-priority 。
func (a *ExecStmt) buildExecutor() (Executor, error) {...... } else {// Do not sync transaction for Execute statement, because the real optimization work is done in// "ExecuteExec.Build".useMaxTS, err := plannercore.IsPointGetWithPKOrUniqueKeyByAutoCommit(ctx, a.Plan)if useMaxTS {if err := ctx.InitTxnWithStartTS(math.MaxUint64); err != nil {return nil, err}}if stmtPri := stmtCtx.Priority; stmtPri == mysql.NoPriority {switch {case useMaxTS:stmtCtx.Priority = kv.PriorityHighcase a.LowerPriority:stmtCtx.Priority = kv.PriorityLow}}}}b := newExecutorBuilder(ctx, a.InfoSchema, a.Ti, a.SnapshotTS, a.IsStaleness, a.ReplicaReadScope)e := b.build(a.Plan)return e, nil
}
3.1.5 TiKV & PD Client Deal
Model Tips:
TiKV Client 被封装在 TiDB 侧,主要承担从 TiKV 获取 KV 数据的作用。
1. 首先 ,通过执行流追溯 func (s *tikvSnapshot) Get(...) --> func (i *TemporaryTableSnapshotInterceptor) OnGet(...) --> func (s *tikvSnapshot) Get(...) 会直接调用封装了 tikv client 的 tikvSnapshot 结构体的 Get(...) 方法,从 TiKV 获取 KV 数据。
2. 其次 ,DEBUG 到 TiKV Client 内部细节会发现 TiKV Client 遵照本地缓存是否存在数据,如果不存在构造请求头,并在 for{} 循环中调用 GetRegionCache() 查询 PD Client 的 Region Cache 获取所要查询 Region 在 TiKV 中的位置,向 TiKV 发送请求获取数据。之所以使用 for 循环,是因为 Region Cache 信息从 PD 获取,所以并不一定是最新的、最准确的 Region Location 信息,包含一些错误重拾操作,如:EpochNotMatch 等等,详情见: Region Cache 缓存和清理逻辑解释 或 TiDB 源码阅读系列文章(十八)tikv-client(上) 。
func (s *KVSnapshot) get(ctx context.Context, bo *retry.Backoffer, k []byte) ([]byte, error) {// Check the cached values first.s.mu.RLock()if s.mu.cached != nil {if value, ok := s.mu.cached[string(k)]; ok {atomic.AddInt64(&s.mu.hitCnt, 1)s.mu.RUnlock()return value, nil}}s.mu.RUnlock()......s.mu.RLock()req := tikvrpc.NewReplicaReadRequest(tikvrpc.CmdGet,&kvrpcpb.GetRequest{Key: k,Version: s.version,}, s.mu.replicaRead, &s.replicaReadSeed, kvrpcpb.Context{Priority: s.priority.ToPB(),NotFillCache: s.notFillCache,TaskId: s.mu.taskID,ResourceGroupTag: s.resourceGroupTag,})s.mu.RUnlock()for {loc, err := s.store.GetRegionCache().LocateKey(bo, k)resp, _, _, err := cli.SendReqCtx(bo, req, loc.Region, client.ReadTimeoutShort, tikvrpc.TiKV, "", ops...)regionErr, err := resp.GetRegionError()val := cmdGetResp.GetValue()return val, nil}
}
3. 最后 ,在 func (e *PointGetExecutor) Next(...) 处理中会通过 isCommonHandleRead
是普通查询还是点查,点查 executor 会直接 get 该 key 的 value。由于 TiDB KV 主要作用是对 TiKV 数据获取处理的封装,便不单独提取模块赘述。
func (e *PointGetExecutor) Next(ctx context.Context, req *chunk.Chunk) error {if e.idxInfo != nil {if isCommonHandleRead(e.tblInfo, e.idxInfo) {handleBytes, err := EncodeUniqueIndexValuesForKey(e.ctx, e.tblInfo, e.idxInfo, e.idxVals)e.handle, err = kv.NewCommonHandle(handleBytes)} else {e.idxKey, err = EncodeUniqueIndexKey(e.ctx, e.tblInfo, e.idxInfo, e.idxVals, tblID)e.handleVal, err = e.get(ctx, e.idxKey)}}......return nil
}
3.2TiKV
下述 3.2 部分均为点查在 TiKV 组件中涉及处理流程介绍。
3.2.1 KV Grpc & Service Deal
1. 首先 ,TiKV 进程启动后,所有的 Grpc 请求处理都由 Service 层接管。位于 src/server/service/kv.rs
文件中,例如本次的点查请求会由 handle_request!(kv_get, future_get, GetRequest, GetResponse);
这样一个声明宏处理,调用 future_get 异步处理。
2. 其次 ,从 Grpc 的请求中解析出 key 等相关信息,作为参数传递调用存储引擎方法 storage.get(...) 进行实际的调用处理,包含构造 snapshot、获取 value 等。
5. 最后 ,在 v(value) 异步结果获取后,构造 Grpc 返回给请求端。
fn future_get<E: Engine, L: LockManager>(storage: &Storage<E, L>,mut req: GetRequest,
) -> impl Future<Output = ServerResult<GetResponse>> {let v = storage.get(req.take_context(),Key::from_raw(req.get_key()),req.get_version().into(),);async move {let mut resp = GetResponse::default();match v {Ok((val, stats)) => {match val {Some(val) => resp.set_value(val),None => resp.set_not_found(true),}}Err(e) => resp.set_error(extract_key_error(&e)),}Ok(resp)}
}
3.2.2 KV Storage ReadPool Deal
1. 首先 ,该函数的作用是从 snapshot 中,搜寻满足 “数据行提交时间戳 < 本次数据行发起请求时间戳” 要求的,无锁的,且 MVCC 多版本数据中时间戳最新的请求行数据 。
2. 其次 ,观察函数逻辑,先会对请求的优先级进行判断,然后从 read_pool 线程池中 spawn 一个 handle,并为该 handle 指定解析出来的 “优先级” 在存储层执行请求,关于 SQL 优先级详见 --> force-priority 说明 。
3. 再次 ,prepare_snap_ctx(...) 会进行内存锁冲突检查,并基于 key、start_ts 等信息构造 snapshot context。
4. 然后 ,通过传入 snap_ctx ,在 let snapshot = Self::with_tls_engine(|engine| Self::snapshot(engine, snap_ctx)).await?;
步构造出存储引擎的 snapshot。
5. 最后 ,并在 let result = snap_store.get(&key, &mut statistics)});
中获取 result 结果。
pub fn get(&self,mut ctx: Context,key: Key,start_ts: TimeStamp,) -> impl Future<Output = Result<(Option<Value>, KvGetStatistics)>> {let priority = ctx.get_priority();let res = self.read_pool.spawn_handle(async move {let snap_ctx = prepare_snap_ctx(&ctx,iter::once(&key),start_ts,&bypass_locks,&concurrency_manager,CMD,)?;let snapshot = Self::with_tls_engine(|engine| Self::snapshot(engine, snap_ctx)).await?;{let snap_store = SnapshotStore::new(snapshot,start_ts,ctx.get_isolation_level(),!ctx.get_not_fill_cache(),bypass_locks,access_locks,false,);let result = snap_store.get(&key, &mut statistics)});Ok((result?,KvGetStatistics {stats: statistics,perf_stats: delta,latency_stats,},))}}.in_resource_metering_tag(resource_tag),priority,thread_rng().next_u64(),);}
3.2.3 KV RocksDB Snapshot Deal
1. 首先 , 从 3.2.2 第 4 步构造 snapshot 部分深入 ,仔细 DEBUG 会发现构造 snapshot 之前 fn async_snapshot(...) 会发起 ReadIndex ,判断此时 leader 是否真的是 leader 。早期 Read Index 是通过发送一次心跳的方式实现的, 关于 read index 详情参考 --> TiKV 功能介绍 - Lease Read 。随后 TiKV 引入 Lease Read 优化, 具体概念及由来参考 --> read index 和 local read 情景分析 ,关于本例点查 Lease Read 部分逻辑在 pub fn propose_raft_command 函数下。
2. 其次 , 从 3.2.2 第 5 步构造 snap_store.get(...) 部分深入 ,对于点查会构造一个 point_getter ,再 get 对应 value,大概逻辑是通过事务的隔离级别分支处理(现阶段所有请求均是 SI 隔离界别)。在 SI 分支中,针对该 User Key 扫一遍 lock CF ,详情参见代码 --> impl<S: Snapshot> PointGetter<S>
; 因为自动提交的点查使用 max_ts,所以这一步会返回空,意味着查询最新的已提交的 Default CF 数据即可。
3. 最后 ,排除锁信息后,进入 load_data(user_key) 会起一个 loop ,构造一个包含 cursor 的WriteRef 不断的扫 Write CF。因为事务提交后,会在 Write CF 中写一个 key 为 {user_key}{commit_ts},value 为 {type}{start_ts} 的记录,所以扫 PUT 类型的 Write CF 意味着可以判断出:查询的数据是否存在 Write CF 中,因为 MVCC 数据读取 指出小于 64 字节的数据会直接内嵌在 Lock Info 或 Write Info,否则调用 load_data_from_default_cf(...) 从 Default CF 中获取查询的结果值。
fn load_data(&mut self, user_key: &Key) -> Result<Option<Value>> {loop {let write = WriteRef::parse(self.write_cursor.value(&mut self.statistics.write))?;match write.write_type {WriteType::Put => {match write.short_value {Some(value) => {// Value is carried in `write`.self.statistics.processed_size += user_key.len() + value.len();println!("-short-->{}<---",String::from_utf8_lossy(value));return Ok(Some(value.to_vec()));}None => {let start_ts = write.start_ts;let value = self.load_data_from_default_cf(start_ts, user_key)?;println!("-default-->{:?}<---",String::from_utf8_lossy(&value));self.statistics.processed_size += user_key.len() + value.len();return Ok(Some(value));}}}}if !self.write_cursor.next(&mut self.statistics.write) {return Ok(None);}}}
3.3 总结
简单来说,主要是点查直接可基于唯一 Key 定位所需 value,查询数据较少,又不用走二级索引回表定位数据。其次 PointGet 跳过了大量优化器的规则优化,直接走了 FastPlan 节省了优化器部分的时间。最后,使得点查成为一个效率较高的执行计划。
四、学习总结
4.1 看源码条件
首先 ,需要清楚看源码的目的,本人作为一个 DBA 出于想了解数据库产品的角度出发,觉得满足如下几点基本就可以开始看源码了。1、2 点是基础,3 点是保持看代码的动力源泉、高效方法。
- 了解基本编程语言语法、语言特性、周边组件,如:rust future、rust 所有权、go gpm、go mod、cargo、Makefile ...... 等等;
- 掌握数据库组件功能、流程、概念,如:解析、编译、执行、火山模型、向量模型、存储模型 ...... 等等;
- 保持持续探索的热情,多与社区及相关爱好者交流,终有一天不懂得模块会被看懂。
最后 ,其实光看代码很多逻辑思想是看不出来的,需结合官网的文章、作者所讲述概念、行业经验才能看出来,否则代码之间跳来跳去很容易迷失方向。
4.2 看代码价值
首先 ,个人觉得看代码的行为对不同角色带来的价值是不同的,比如:
1. 作为数据库内核开发角色,看数据库代码是基本技能,日常工作中了解不同产品特性 ... 2.作为 DBA 角色,可以看到更多逻辑概念下的细节,尤其是在产品较不成熟(文档好少、BUG 较多、最佳实践较少)的情况下,在遇到问题时,进一步深入细节发现解决问题的新思路 ... 3. 作为应用开发角色,可以发现代码实现手段上的“黑科技”,更好依据不同数据库特性、用好不同数据库产品 ...
其次 ,谈到价值,个人觉得不可忽略的是 “投入产出比” 。增加这一衡量因素后,不禁如下疑问从脑中产生:
1. 该行为如果只是觉得很牛,这件事是否真的值得做?如果做了,可能只是为了满足内心对于某件事的好奇,说明是根本没有衡量该行为的价值,属于冲动行为的结果。当然结果可能是好的,也可能是坏的。 2. 该行为对 DBA 角色来讲,带来的收益到底有多大?代码看的多可能对逻辑概念、产品特性认知的更准确、牢固,可是这种老牢固与阅读故障案例相比,在同等时间投入下获得的收益 “谁胜谁负” 恐怕是个问号❓。再者,真出现了产品问题,没有产品作者的支持与确认我真的敢改代码、或者下定论吗? 3. 行业 或 企业对于 DBA 角色的阅读代码技能是怎样定位的?涉及代码问题处理的工作大可以由专业的人负责,专业的人做专业的事效率可能更高效些。如果假设该前提是正确的,那么 DBA 阅读代码无疑是一种低效的行为。 4. 如果为了构建自己写代码的能力,为什么不去做研发?哈哈哈哈哈 ......
最后 ,上述种种问题现在我只能提出,而不能回答,也没有资格回答,本人也在不断探索、学习、抉择中 ... ,也可能这件事情没有一个绝对的答案。
总结下来,本片文章在 TiDB 产品层面描述了数据库逻辑概念下,点查行为和代码实现的关联关系,可以进一步提高作者对产品的认知。在看代码学习行为层面,简述了个人对该行为所带来价值多少的衡量。也许,这是本文啰唆表达下所能带来的 2 点 “仅有的” 价值。
五、引用
TiDB Blog -- TiDB 源码阅读系列文章(二)初识 TiDB 源码
TiDB Blog -- TiDB 源码阅读系列文章(十三)索引范围计算简介
TiDB Blog -- TiDB 源码阅读系列文章(三)SQL 的一生
TiDB Blog -- TiKV 源码解析系列文章(十九)read index 和 local read 情景分析
TiDB Blog -- TiKV 功能介绍 - Lease Read
TiDB Blog -- TiKV 源码解析系列文章(十三)MVCC 数据读取
TiDB Blog -- TiDB 源码阅读系列文章(十八)tikv-client(上)
Jack Yu Blog -- 如何阅读 TiDB 的源代码(一)
Jan Su Blog -- TiDB run and debug on M1
MySQL Doc -- MySQL Client/Server Protocol
MySQL Doc -- MySQL协议分析
Talkgo movie -- TiDB 源码阅读之 Compiler【有彩蛋哦】【 Go 夜读 】
AskTUG Req -- tidb sql 执行
Zhihu Blog -- RocksDB事务实现TransactionDB分析
Zhihu Blog -- SQL 优化之火山模型
\
\
\
\
\
\
\
\
\
\
\
\
\
PointGet的一生相关推荐
- 受用一生的高效 PyCharm 使用技巧(六)
http://www.sohu.com/a/329854019_654419 大家好,今天我又来给大家更新 PyCharm 的使用技巧. 从第一篇开始,一直到本篇,一共更新了6篇文章,每篇 5 个小技 ...
- 受用一生的高效 PyCharm 使用技巧(四)
https://blog.csdn.net/pdcfighting/article/details/93269028 大家好,距离最近一篇 PyCharm 使用技巧的文章已经过去一月有余,最近虽然也比 ...
- 受用一生的高效 PyCharm 使用技巧(二)pycharm 指定参数运行文件
https://mp.weixin.qq.com/s/Ii0-qHUXayTPb-K-17hmQQ 在介绍技巧之前,有些话想声明一下,这个系列的一些小技巧,对于一些重试用户来说可能是小 case,如果 ...
- 场面话大全,绝对受用一生
◆ 父母生日祝酒辞 尊敬的各位领导.各们长辈.各们亲朋好友:大家好! 在这喜庆的日子里,我们高兴地迎来了敬爱的父亲(母亲)XX岁的生日.今天,我们欢聚一堂,举行父亲(母亲)XX华诞庆典.这里,我代 ...
- Mr. Process的一生-Linux内核的社会视角 (2)启动
原文地址: http://www.manio.org/cn/startup-of-linux-view-of-society.html 其实这才应该是这一系列文章的第一节,因为这篇文章讲的是盘古开天地 ...
- 毕业后五年之内将决定你的一生
大家千万不要错过这篇文章,毕业2年多了,能看到这篇文章也是一种幸运,真的受益匪浅,对我有很大启迪,这篇文章将会改变我的一生,真的太好了,希望与有缘人分享,也希望对有缘人有所帮助!看完之后有种" ...
- 人的一生有三件事不能等
人的一生有三件事不能等 人的一生有三件事不能等 第一是"贫穷" 贫穷不能等,因为一但时间久了,你将习惯贫穷,到时不但无法突破自我,甚至会抹杀了自己的梦想,而庸庸碌碌的过一辈子... ...
- QWidget一生,从创建到销毁事件流
版权声明:若无来源注明,Techie亮博客文章均为原创. 转载请以链接形式标明本文标题和地址: 本文标题:QWidget一生,从创建到销毁事件流 本文地址:http://techieliang ...
- 中科大硬核“毕业证”:“一生一芯”计划下,5位本科生带自研芯片毕业
作者 | 包云岗 编辑 | 伍杏玲 本文经作者授权转载自包云岗知乎 [CSDN编者按]近日,中国科学院大学五位本科生的硬核"毕业证"引发IT圈热议,在"一生一芯" ...
最新文章
- NPTL简介 (NATIVE POSIX Thread Library)
- 解析mysqlbinlog日志_每日学点---Mysql的binlog日志解析导出
- python中输入17=x会引起错误_python新手常犯的17个错误
- [蓝桥杯][算法提高VIP]贪吃的大嘴(多重背包)
- 开发中的问题——环境相关
- 51Nod 1105 第K大的数 二分答案
- Linux Ubuntu jdk(环境变量)配置
- python删除第一行_python3.7 openpyxl 删除指定一列或者一行的代码
- 大厂面试常问的机器学习,计算机视觉怎么学?详细指南来了!
- 探讨VSTS联合MS PROJECT协同开发之三:比较篇
- map赋值给另一个map_如何写出一个能让面试官直呼“666”的深拷贝?
- windows下7z文件解压
- matlab 读取 Microsoft Excel 电子表格文件不推荐使用 xlsread
- oracle11g 32021,64ビットのOracle Data Access Components(ODAC)のダウンロード
- 人力资源管理系统如何促进业务增长
- prometheus命令_Prometheus配置
- 从功能到年薪30W+的测试开发工程师,分享我这10年的职业规划路线
- android view硬件加速,Android TextureView和硬件加速
- Windowns 离线安装WSL2
- 2022高教社杯全国大学生数学建模竞赛(国赛)A题国家二等奖论文及支撑代码