最新要闻
- 蔚来全系降价3万!未必心甘情愿 但又无可奈何:被大众收购才是出路?
- 4990元起!雅迪冠能探索E10发布:2000W电机、极速62Km/h 最资讯
- 欠薪过亿!时隔两月 宝能汽车总部又被“堵门”_每日视讯
- 《守望2》新付费PVE让玩家不爽:暴雪你还有脸要钱呢? 热头条
- 李想:并没把国内友商放在眼里 理想的目标是干BBA
- 天天快资讯:澳科大领衔的研究团队开发出新型人工智能医疗诊断模型
- M2 Ultra加持!苹果新款Mac Studio评测:5万元的恐怖生产力
- 当前速看:马斯克称人类已经是半机器人:大脑思维上传服务器可永生
- 世界热讯:Arm发布全新智能视觉参考设计 首次整合第三方IP核心
- “上四休三”的老板后悔了 有员工选择混:这制度对国人不适合? 环球快消息
- 央行下调常备借贷便利利率
- 公开反对电动汽车后:丰田低头了
- 云南金平迎来蝴蝶大爆发:近亿只集中羽化 犹如纷飞落叶-世界观焦点
- 支持电视端:爱奇艺白金会员年卡+京东PLUS会员年卡248元
- 世界观点:SK-II回应神仙水是否有核污染:符合国家标准
- 每日报道:12代及以上CPU性能更强了!Intel为Linux开发新调度补丁
广告
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
MySql的MVCC机制
(资料图片)
事务隔离级别遗留问题:
在读已提交的级别下,事务B可以读到事务A持有写锁的的记录,且读到的是未更新前的,为何写读没有冲突?
可重复读级别,事务B可以更新事务A理论上应该已经获取读锁的记录,且更新后,事务A依然可以读到数据,为何读-写-读没有冲突?
- 在可重复读级别,幻读没有产生
其中,前两个问题就是因为mvcc机制(读锁的一种优化机制),通过不加读锁,避免读写冲突,进而提高了性能。
为什么要有MVCC机制?
在读已提交的级别下,由于是给读加锁来保证读已提交, 如果事务A持有写锁,为了保证读已提交,事务B必须等待事务A提交之后才可以读;其他的读事务也是这样的情况,效率太低
在可重复读级别,为了保证可重复读,如果事务A持有读锁,为了第二次读到的一样,其他所有写事务必须等待读完才可以,同样效率低
那么很自然的想到,无论读事务是先产生还是后产生,如果这个时候还存在写事务没有执行,或者需要执行;那么就应该让读事务读到目前最新的值,且写事务可以更新;只不过读事务在写事务提交更新后,依据隔离级别是否可见最新更新即可。这就是MVCC机制的核心能力,将读锁干掉。
MVCC机制核心组件
MVCC机制由版本链、undolog、readview三大核心构成版本链
猜测很多人第一次看到MVCC的版本都是和我一样在各种各样的博客文章上,或者可能是在一些课程专栏或者《高性能mysql》这本书的mvcc部分看到的,那么在你的理解中,版本的底层是什么样子呢?innodb引擎数据库中的每一条记录上,我们都可以认为上面有3个隐藏字段,分别是DB_ROW_ID(不在此次讨论范围),DB_TRX_ID和DB_ROLL_PTR,如下图一样在我的理解中,DB_TRX_ID就是插入或者更新时,当前事务的trx_id,由全局事务管理器分配的递增的一个id;DB_ROLL_PTR存储的undolog中当前记录上一个版本的指针,先姑且记住这是一个指针。当插入一条记录时在这条记录的DB_TRX_ID填入当前事务的id,由于没有历史版本,所以DB_ROLL_PTR为空当更新一条记录时由于这个时候存在历史版本,所以需要将老版本的数据写到undolog里,然后构建指针,将DB_TRX_ID更新为当前事务的id,将DB_ROLL_PTR更新为刚才构建的指针,以及更新需要更新的字段。当删除一条记录时(这个不太确定,主观猜测)猜测是将老记录写到undolog,然后构建指针,新记录DB_TRX_ID更新为当前事务的id,将DB_ROLL_PTR更新为刚才构建的指针,但是没有需要更新的字段。而且mysql不会立即删除,记录上有一个info_bits字段,会标记上删除标识(REC_INFO_DELETED_FLAG),后续由purge线程(不了解,姑且认为是个scheduleTask吧)删除这样,当多次更新之后,新记录存储的永远都是最新操作的事务id,并通过指针指向了老版本,老版本还指向了更老的版本...等等,最终构成了一个版本链Readview
理论:
在周志明老师的凤凰架构(或者极客时间的‘周志明的软件架构课’)中对mvcc简单介绍到隔离级别是可重复读:总是读取 CREATE_VERSION 小于或等于当前事务 ID 的记录,在这个前提下,如果数据仍有多个版本,则取最新(事务 ID 最大)的。隔离级别是读已提交:总是取最新的版本即可,即最近被 Commit 的那个版本的数据记录。在mysql官网中是这么描述的
If the transaction isolation level is翻译:隔离级别是可重复读:在同一个事务中,一致性读总是去读在该事务第一次读取时生成的快照。隔离级别是读已提交:事务中的每次读取都取自己新生成的快照。相比之下,周老师形容的更贴近隔离级别的概念上,官方的描述则是底层的具体实现逻辑。两者结合一下就是可重复读:通过在每个事物只读取第一次select时生成的快照和undolog比较,根据一个可见性规则判断,是否可以读当前版本的记录,可以就返回,不行就继续比较再上一个版本,直到最老的版本;读已提交:除了每次读取都会使用最新的快照,后面的都和可重复读的逻辑一样。为什么我这里说的是可见性规则呢?是因为周老师描述里“总是读取 CREATE_VERSION 小于或等于当前事务 ID 的记录”很容易错误的理解为当前版本记录里的trx_id<=快照创建时的事务id(create_trx_id)就都可见,真正的判断逻辑并不只是一个create_trx_id就能搞定的。但这里先不展开讲,自己想一下为什么不行,下面的图可能会给你一点灵感,接下来我们先去读一下“可见性规则”的底层源码。REPEATABLE READ
(the default level), all consistent reads within the same transaction read the snapshot established by the first such read in that transaction. You can get a fresher snapshot for your queries by committing the current transaction and after that issuing new queries.WithREAD COMMITTED
isolation level, each consistent read within a transaction sets and reads its own fresh snapshot.
可见性规则底层实现
ReadView类
storage/innobase/include/read0types.h:47//ReadView类class ReadView {...private: /** trx id of creating transaction, set to TRX_ID_MAX for free views. */ //创建快照的时候,快照对应的事务id,只有含有写操作的才会分配真正的事务id trx_id_t m_creator_trx_id;/** Set of RW transactions that was active when this snapshot was taken */ //活跃的读写事务id列表,从trx_sys->rw_trx_ids抄过来的 ids_t m_ids;/** The read should not see any transaction with trx id >= this value. In other words, this is the "high water mark". */ //赋值是即将分配的下一个事务id,所以大于等于这个id的记录对当前事务来说都是不可见的 trx_id_t m_low_limit_id;/** The read should see all trx ids which are strictly smaller (<) than this value. In other words, this is the low water mark". */ //m_ids不为空就是ids.get(0),为空则是m_low_limit_id,所以小于这个事务id的就代表着快照建立的时候 //已经不是活跃事务了,即已经提交了,所以一定可以看到这些事务的改动记录 trx_id_t m_up_limit_id;.... }
初始化赋值的时候
//read0read.cc//row_search_mvcc -> trx_assign_read_view -> MVCC::view_open ->void ReadView::prepare(trx_id_t id) { ut_ad(trx_sys_mutex_own()); m_creator_trx_id = id; m_low_limit_no = trx_get_serialisation_min_trx_no(); m_low_limit_id = trx_sys_get_next_trx_id_or_no(); ut_a(m_low_limit_no <= m_low_limit_id); if (!trx_sys->rw_trx_ids.empty()) { copy_trx_ids(trx_sys->rw_trx_ids); } else { m_ids.clear(); }/* The first active transaction has the smallest id. */ m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id; ut_a(m_up_limit_id <= m_low_limit_id); ut_d(m_view_low_limit_no = m_low_limit_no); m_closed = false;}
判断某个版本的记录是否可见?
//read0types.h bool changes_visible(trx_id_t id, const table_name_t &name) const { ut_ad(id > 0);//如果当前版本记录上的事务id(DB_TRX_ID)小于低水位或者等于当前事务,//那么要么就是自己更改的,要么就是历史上已经提交了的,所以可以读到 if (id < m_up_limit_id || id == m_creator_trx_id) { return (true); } check_trx_id_sanity(id, name);//如果当前版本记录上的事务id(DB_TRX_ID)大于高水位,那么就是在当前快照生成后生成的事务,一律看不到 if (id >= m_low_limit_id) { return (false); //这一步我没有理解, } else if (m_ids.empty()) { return (true); } const ids_t::value_type *p = m_ids.data();//二分查找,如果活跃的事务里面没有,那么就返回true//这里我是这么理解的,[低水位,高水位]包含活水和死水,即活跃的事务和已经提交的事务//假如存在事务1是活跃的,事物2是已提交的,事务3是活跃的,我们在事务4的时候开启快照,很明显我们只能读到事务2或者事务4的变更//假如正在判断的是事务2,因为已经经过了上面的校验,//所以我们知道当前版本记录的事务m_low_limit_id(高水位)>id>=m_up_limit_id(低水位),且不是当前事务;//所以就需要判断事务只要不是活跃的,那么就一定是已经提交的事务,那么就可读 return (!std::binary_search(p, p + m_ids.size(), id));}在了解可见性规则之后我们知道,快照建立的时候会初始化几个属性,在查询的时候会通过changes_visible方法来判断是否可见,而调用这个方法的上层就是下面这两段逻辑,和我先前描述的类似,判断是否可以读当前版本的记录,可以就返回,不行就继续比较再上一个版本,直到最老的版本
//row0sel.cc#row_search_mvccif (srv_force_recovery < 5 && !lock_clust_rec_cons_read_sees(rec, index, offsets, trx_get_read_view(trx))) { rec_t *old_vers; /* The following call returns "offsets" associated with "old_vers" */ err = row_sel_build_prev_vers_for_mysql( trx->read_view, clust_index, prebuilt, rec, &offsets, &heap, &old_vers, need_vrow ? &vrow : nullptr, &mtr, prebuilt->get_lob_undo()); if (err != DB_SUCCESS) { goto lock_wait_or_error; } if (old_vers == nullptr) { /* The row did not exist yet in the read view */ goto next_rec; } rec = old_vers; prev_rec = rec; ut_d(prev_rec_debug = row_search_debug_copy_rec_order_prefix( pcur, index, prev_rec, &prev_rec_debug_n_fields, &prev_rec_debug_buf, &prev_rec_debug_buf_size));}//lock0lock.cc#lock_clust_rec_cons_read_seesbool lock_clust_rec_cons_read_sees( const rec_t *rec, /*!< in: user record which should be read or passed over by a read cursor */ dict_index_t *index, /*!< in: clustered index */ const ulint *offsets, /*!< in: rec_get_offsets(rec, index) */ ReadView *view) /*!< in: consistent read view */{ ut_ad(index->is_clustered()); ut_ad(page_rec_is_user_rec(rec)); ut_ad(rec_offs_validate(rec, index, offsets)); /* Temp-tables are not shared across connections and multiple transactions from different connections cannot simultaneously operate on same temp-table and so read of temp-table is always consistent read. */ if (srv_read_only_mode || index->table->is_temporary()) { ut_ad(view == nullptr || index->table->is_temporary()); return (true); } /* NOTE that we call this function while holding the search system latch. */ trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets); return (view->changes_visible(trx_id, index->table->name));}
事务的trx_id
在我还没开始看mysql源码,只是跟着博客学习写用例测试的时候,我发现,开启事务进行了第一次查询之后,确实有生成事务id,但后面我执行了一条更新语句之后,原来的事务id变了;就像下面这个图一样,最开始只有查询的时候是比较长的这个id,但执行了一条update语句后,事务id变成了一个短的。这个时候我就产生了很多疑问?同一个事务里,事务id怎么还能变呢?变的话changes_visible里面的比较怎么算?搜索了一下之后了解到,只读事务是不会生成事务id的,是假的!于是我又疑惑,那这个假id怎么参与changes_visible呢?也就是这个时候,我才下定决心去看源码,也借此理解了高低水位的设计,并认识到自己之前的理解是错误的。先上结论只读事务不会分配真正的事务id,他的值是0;只读事务参与change_visable的时候,create_trx_id也确实是0,是通过m_up_limit_id(低水位)来判断是否可见的,只有在变成读写事务时,create_trx_id才会起效并应用;因为值是0,所以在通过下面sql查询的时候,那串id只是展示的时候特殊处理的select * from information_schema.INNODB_TRX;
//trx0trx.cc#trx_start_low //这里可以看到只有读写事务才真正分配了id else {trx->id = 0;if (!trx_is_autocommit_non_locking(trx)) { /* If this is a read-only transaction that is writing to a temporary table then it needs a transaction id to write to the temporary table. */ if (read_write) { trx_sys_mutex_enter(); ut_ad(!srv_read_only_mode); trx->state.store(TRX_STATE_ACTIVE, std::memory_order_relaxed); trx->id = trx_sys_allocate_trx_id(); trx_sys->rw_trx_ids.push_back(trx->id); trx_sys_mutex_exit(); trx_sys_rw_trx_add(trx); } else { trx->state.store(TRX_STATE_ACTIVE, std::memory_order_relaxed); } } else { ut_ad(!read_write); trx->state.store(TRX_STATE_ACTIVE, std::memory_order_relaxed); }}//trx0trx.ic//这里是在展示的时候对只读事务的id做了处理/** Retreieves the transaction ID.In a given point in time it is guaranteed that IDs of the runningtransactions are unique. The values returned by this function for readonlytransactions may be reused, so a subsequent RO transaction may get the same IDas a RO transaction that existed in the past. The values returned by thisfunction should be used for printing purposes only.@param[in] trx transaction whose id to retrieve@return transaction id */static inline trx_id_t trx_get_id_for_print(const trx_t *trx) { /* Readonly and transactions whose intentions are unknown (whether they will eventually do a WRITE) don"t have trx_t::id assigned (it is 0 for those transactions). Transaction IDs in information_schema.innodb_trx.trx_id, performance_schema.data_locks.engine_transaction_id, performance_schema.data_lock_waits.requesting_engine_transaction_id, performance_schema.data_lock_waits.blocking_engine_transaction_id should match because those tables could be used in an SQL JOIN on those columns. Also trx_t::id is printed by SHOW ENGINE INNODB STATUS, and in logs, so we must have the same value printed everywhere consistently. */ /* DATA_TRX_ID_LEN is the storage size in bytes. */ static const trx_id_t max_trx_id = (1ULL << (DATA_TRX_ID_LEN * CHAR_BIT)) - 1; ut_ad(trx->id <= max_trx_id); /* on some 32bit architectures casting trx_t* (4 bytes) directly to trx_id_t (8 bytes unsigned) does sign extension and the resulting value has highest 32 bits set to 1, so the number is unnecessarily huge. Also there is no guarantee that we will obtain the same integer each time. Casting to uintptr_t first, and then extending to 64 bits keeps the highest bits clean. */ return (trx->id != 0 ? trx->id : trx_id_t{reinterpret_cast(trx)} | (max_trx_id + 1));}
生成快照时机(不太确定)
可重复读:只生成一次,后面继续使用ReadView *trx_assign_read_view(trx_t *trx) /*!< in/out: active transaction */{ ut_ad(trx_can_be_handled_by_current_thread_or_is_hp_victim(trx)); ut_ad(trx->state.load(std::memory_order_relaxed) == TRX_STATE_ACTIVE); if (srv_read_only_mode) { ut_ad(trx->read_view == nullptr); return (nullptr); } else if (!MVCC::is_view_active(trx->read_view)) { trx_sys->mvcc->view_open(trx->read_view, trx); } return (trx->read_view);}读已提交:用完就关,所以每次再获取就得新开,但是这里的关有两个地方调
ha_innodb.cc#store_lock 和ha_innodb.cc#external_lockif (lock_type != TL_IGNORE && trx->n_mysql_tables_in_use == 0) { trx->isolation_level = innobase_trx_map_isolation_level(thd_get_trx_isolation(thd)); if (trx->isolation_level <= TRX_ISO_READ_COMMITTED && MVCC::is_view_active(trx->read_view)) { /* At low transaction isolation levels we let each consistent read set its own snapshot */ mutex_enter(&trx_sys->mutex); trx_sys->mvcc->view_close(trx->read_view, true); mutex_exit(&trx_sys->mutex); }}
在学习了解MVCC机制中遇到的问题:
- 为什么更新操作必须使用当前读?
- 只读事务突然更新的话,因为更新必须使用当前读,那是否需要重新生成事务id?
- 只读事务分配的事务id是什么东西?如何参与运作?
- readview的范围
- 知道了mvcc底层是undolog和readview后,怎么理解“版本”这个概念
- 在只读视图能查到其他事务已经删除并且提交的记录吗?
怎么解决的幻读?
在只读事务下,如上文所说的事务1读不到事务2的更新是因为事务2的版本号要大于当前快照的高水位,那对于新增的记录来说,其版本号也是同样的道理,因此事务1读不到比当前快照里的高水位高的,也就避免了幻读这种情况。参考资料:
MySQL 8.0 MVCC 源码解析 - 掘金https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.htmlMySQL事务ID的分配时机_mysql事务id什么时候分配_哲学长的博客-CSDN博客MYSQL innodb中的只读事物以及事物id的分配方式_ITPUB博客Mysql如何实现隔离级别 - 可重复读和读提交 源码分析_mysql 可重复度源码_择维士的博客-CSDN博客关键词:
MySql的MVCC机制
.NET 使用ILPack组件将程序集保存成dll 世界球精选
蔚来全系降价3万!未必心甘情愿 但又无可奈何:被大众收购才是出路?
4990元起!雅迪冠能探索E10发布:2000W电机、极速62Km/h 最资讯
欠薪过亿!时隔两月 宝能汽车总部又被“堵门”_每日视讯
《守望2》新付费PVE让玩家不爽:暴雪你还有脸要钱呢? 热头条
李想:并没把国内友商放在眼里 理想的目标是干BBA
现代C++学习指南-方向篇|焦点速读
文心一言 VS 讯飞星火 VS chatgpt (40)-- 算法导论5.4 4题|天天快播
天天快资讯:澳科大领衔的研究团队开发出新型人工智能医疗诊断模型
M2 Ultra加持!苹果新款Mac Studio评测:5万元的恐怖生产力
当前速看:马斯克称人类已经是半机器人:大脑思维上传服务器可永生
世界热讯:Arm发布全新智能视觉参考设计 首次整合第三方IP核心
“上四休三”的老板后悔了 有员工选择混:这制度对国人不适合? 环球快消息
央行下调常备借贷便利利率
MySQL索引
OnceCell和OnceLock的介绍-每日时讯
公开反对电动汽车后:丰田低头了
云南金平迎来蝴蝶大爆发:近亿只集中羽化 犹如纷飞落叶-世界观焦点
支持电视端:爱奇艺白金会员年卡+京东PLUS会员年卡248元
世界观点:SK-II回应神仙水是否有核污染:符合国家标准
每日报道:12代及以上CPU性能更强了!Intel为Linux开发新调度补丁
张轩昊丨操作思路分享【6月13日】今日黄金原油操作建议,实盘交易干货分享!
焦点速看:用纯HTML,JS,CSS实现横向滚动标签页
CSS常用属性
腾势N7盲订破2万台 增换购用户55%来自BBA等豪华品牌
世界短讯!特斯拉充电标准一天内被四家公司接受 网友:马始皇?
学习Win12好榜样?国产OS系统deepin成立AI社区:未来更智能_世界热头条
69元 小米车载充电器特惠开售:100W快充 配有炫彩灯效 时讯
环球热讯:历史性变革 大众汽车计划进行数十年来最大规模重组
年仅52岁!知名男演员在睡梦中去世... 今日热讯
国内第四大运营商 中国广电终于支持携号转网:但只入不出
刚考完科目一就买车上路 男子:想提前预习科目二、科目三
还说5G没用?全国5G平均下行速率348Mbps 比4G快6倍
环球视讯!被动散热靠谱么?博主实测15英寸MacBook Air性能表现
【新要闻】蔚来号召员工为车主献血 称“伙伴们都在积极报名”
焦点速讯:洛阳老城区:拆违治乱顺民意
【快播报】Collections类源码初探
天天微资讯!2023油价调整窗口时间表一览
马斯克 一统海外充电江湖!
新增自动拍摄功能:尼康Z9获4.00版本固件更新
90后小伙中1000万大奖 淡定回应先去上个班:不着急领
看热讯:阵容强大!长城汽车17位高管集体入驻微博:吉利副总裁盛赞
史低价!小米米家无线洗地机2开启618预售:仅1799元-当前视点
视点:过山车行情后,建筑钢材市场行情或易涨难跌
今日精选:Koordinator 最佳实践系列:精细化 CPU 编排
天天观察:Android RIL&IMS源码分析
天天即时:5款超级好用的开发效率工具,建议收藏!
世界观点:记录--为什么推荐用svg而不用icon?
油价小幅下调 加一箱油将少花2元 全球最资讯
惠誉博华:48家银行未赎回二级资本债券,合计达364.8亿元
恒生指数收涨0.6% 恒生科技指数大涨重回4000点上方
中海达(300177)6月13日主力资金净买入525.51万元_天天通讯
PMR机械硬盘走到尽头了!西数/希捷正疯狂自救
6旬老人守株待兔式飞奔撞车碰瓷索赔 车主吓坏:监控还清白 环球精选
广东有多热?网友在广东买虾 还没到家就熟了 世界今日讯
美国再将31家中企列入“实体清单”!
2999元的国产显卡值不值得冲?实测3A大作给你答案
焦点信息:天津:“多卡合一”创新服务“城市小蜜蜂”
今日热文:线段树学习笔记
每日速看!尚医通-day10【微信扫码登录】(内附源码)
linux iptables安全技术与防火墙_快播报
收评:创业板指涨0.68%收获三连阳 半导体行业涨幅靠前
全球快报:小米卢伟冰:小米13 Ultra在意法西德及香港地区正式开售 销售超预期
首发预装鸿蒙OS 4.0!华为Mate60 Pro概念图出炉 全球热资讯
天天精选!NASA决定造访遍地黄金的“灵神星”:平均每位美国人能分300亿美元
美系硬派SUV福特探险者谍照曝光!外观内饰全面升级 预计将在年内首次亮相_今日热讯
今日热搜:8GB显卡卖到3199元 显存成本曝光:英伟达实在太赚了
视点!男生抠掉脸上痘痘流血近1小时:用了一包400张抽纸
每日报道:6月13日华鲁恒升尿素价格暂稳
焦点信息:市场监管总局:瞄准先进材料、人工智能等领域推动建立国家标准参考数据中心
烟台大学附属中学石明校区举行垃圾分类科普讲座|天天时快讯
速递!8GB内存笔记本卖到10499元起 苹果被批吃相难看:应该破发
货车高速上连续疯狂别车被撞停 官方:未造成伤亡、已找到肇事者|当前播报
天天热讯:马斯克相中的男人!14岁成SpaceX最年轻工程师、岗位年薪百万
环球即时看!小米平板6 Pro两个月使用心得:找不到短板的安卓板皇
Arm发布全新智能视觉参考设计 首次整合第三方IP核心
2023安洵杯 re复现
每天一道面试题:Spring的Bean生命周期
Axure RP教程_编程入门自学教程_菜鸟教程-免费教程分享_环球今日讯
环球今日报丨哥伦比亚4名空难获救儿童的母亲生前或遭家暴,孩子外公和父亲欲争夺抚养权
当前关注:你收益多少?余额宝上线第十年:每天为国人赚1亿零花钱 网友狂晒单
排队5小时!浙江网红面包黄牛加价上百元 网友吐槽:消协回应
【当前独家】中轴线文化遗产有了常设讲堂
三种方法让.NET轻松实现Excel转PDF
天天快看点丨docker-compose搭建wordpress
【播资讯】比亚迪执行副总裁:美国市场不在我们考虑范围内
石家庄迈入“刷脸”乘车时代:买一根火腿肠就能免费坐地铁活动结束了_环球动态
制作成本16.5亿!《封神三部曲》第一部7月20上映:角色海报公布 太强大 今日报
吹牛还是玩真的?丰田下一代电动汽车续航达1500公里
【天天报资讯】小米发布米家旅行箱:顶部嵌平设计 行走的小桌板
市场监管总局:到2035年 计量数据归集共享规模显著提升-快播
【技术积累】软件设计模式中的工厂模式【一】-独家
STM32F429 Discovery开发板应用:使用FreeRTOS队列+DMA双缓存实现串口数据接收
【寻味中华丨饮食】蔡甸藕带:白若玲珑玉 丝缕皆故乡
【天天时快讯】699元!XREAL Beam投屏盒子发布:随身携带的“可悬停AR空间屏”
AMD今晚发布新CPU Intel急了:至强性能比EPYC快7倍
【世界新视野】4-1战胜热火!掘金队夺队史首个NBA总冠军:网友发帖祝贺 约老师太强
小区门口连装8条减速带 物业回复让业主无语:为防业主逃费
贵1000元值不值?i7-13700H和i5-13500H对比实测 世界观点