开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

庖丁解InnoDB之REDO LOG 一 为什么需要记录RE

发表于 2021-11-12

简介: 数据库故障恢复机制的前世今生一文中提到,今生磁盘数据库为了在保证数据库的原子性(A, Atomic) 和持久性(D, Durability)的同时,还能以灵活的刷盘策略来充分利用磁盘顺序写的性能,会记录REDO和UNDO日志,即ARIES方法。本文将重点介绍REDO LOG的作用,记录的内容,组织结构,写入方式等内容,希望读者能够更全面准确的理解REDO LOG在InnoDB中的位置。本文基于MySQL 8.0代码。

作者 | 瀚之

来源 | 阿里技术公众号

数据库故障恢复机制的前世今生一文中提到,今生磁盘数据库为了在保证数据库的原子性(A, Atomic) 和持久性(D, Durability)的同时,还能以灵活的刷盘策略来充分利用磁盘顺序写的性能,会记录REDO和UNDO日志,即ARIES方法。本文将重点介绍REDO LOG的作用,记录的内容,组织结构,写入方式等内容,希望读者能够更全面准确的理解REDO LOG在InnoDB中的位置。本文基于MySQL 8.0代码。

一 为什么需要记录REDO

为了取得更好的读写性能,InnoDB会将数据缓存在内存中(InnoDB Buffer Pool),对磁盘数据的修改也会落后于内存,这时如果进程或机器崩溃,会导致内存数据丢失,为了保证数据库本身的一致性和持久性,InnoDB维护了REDO LOG。修改Page之前需要先将修改的内容记录到REDO中,并保证REDO LOG早于对应的Page落盘,也就是常说的WAL,Write Ahead Log。当故障发生导致内存数据丢失后,InnoDB会在重启时,通过重放REDO,将Page恢复到崩溃前的状态。

二 需要什么样的REDO

那么我们需要什么样的REDO呢?首先,REDO的维护增加了一份写盘数据,同时为了保证数据正确,事务只有在他的REDO全部落盘才能返回用户成功,REDO的写盘时间会直接影响系统吞吐,显而易见,REDO的数据量要尽量少。其次,系统崩溃总是发生在始料未及的时候,当重启重放REDO时,系统并不知道哪些REDO对应的Page已经落盘,因此REDO的重放必须可重入,即REDO操作要保证幂等。最后,为了便于通过并发重放的方式加快重启恢复速度,REDO应该是基于Page的,即一个REDO只涉及一个Page的修改。

熟悉的读者会发现,数据量小是Logical Logging的优点,而幂等以及基于Page正是Physical Logging的优点,因此InnoDB采取了一种称为Physiological Logging的方式,来兼得二者的优势。所谓Physiological Logging,就是以Page为单位,但在Page内以逻辑的方式记录。举个例子,MLOG_REC_UPDATE_IN_PLACE类型的REDO中记录了对Page中一个Record的修改,方法如下:

(Page ID,Record Offset,(Filed 1, Value 1) … (Filed i, Value i) … )

其中,PageID指定要操作的Page页,Record Offset记录了Record在Page内的偏移位置,后面的Field数组,记录了需要修改的Field以及修改后的Value。

由于Physiological Logging的方式采用了物理Page中的逻辑记法,导致两个问题:

1、需要基于正确的Page状态上重放REDO

由于在一个Page内,REDO是以逻辑的方式记录了前后两次的修改,因此重放REDO必须基于正确的Page状态。然而InnoDB默认的Page大小是16KB,是大于文件系统能保证原子的4KB大小的,因此可能出现Page内容成功一半的情况。InnoDB中采用了Double Write Buffer的方式来通过写两次的方式保证恢复的时候找到一个正确的Page状态。这部分会在之后介绍Buffer Pool的时候详细介绍。

2、需要保证REDO重放的幂等

Double Write Buffer能够保证找到一个正确的Page状态,我们还需要知道这个状态对应REDO上的哪个记录,来避免对Page的重复修改。为此,InnoDB给每个REDO记录一个全局唯一递增的标号LSN(Log Sequence Number)。Page在修改时,会将对应的REDO记录的LSN记录在Page上(FIL_PAGE_LSN字段),这样恢复重放REDO时,就可以来判断跳过已经应用的REDO,从而实现重放的幂等。

三 REDO中记录了什么内容

知道了InnoDB中记录REDO的方式,那么REDO里具体会记录哪些内容呢?为了应对InnoDB各种各样不同的需求,到MySQL 8.0为止,已经有多达65种的REDO记录。用来记录这不同的信息,恢复时需要判断不同的REDO类型,来做对应的解析。根据REDO记录不同的作用对象,可以将这65中REDO划分为三个大类:作用于Page,作用于Space以及提供额外信息的Logic类型。

1、作用于Page的REDO

这类REDO占所有REDO类型的绝大多数,根据作用的Page的不同类型又可以细分为,Index Page REDO,Undo Page REDO,Rtree PageREDO等。比如MLOG_REC_INSERT,MLOG_REC_UPDATE_IN_PLACE,MLOG_REC_DELETE三种类型分别对应于Page中记录的插入,修改以及删除。这里还是以MLOG_REC_UPDATE_IN_PLACE为例来看看其中具体的内容:

其中,Type就是MLOG_REC_UPDATE_IN_PLACE类型,Space ID和Page Number唯一标识一个Page页,这三项是所有REDO记录都需要有的头信息,后面的是MLOG_REC_UPDATE_IN_PLACE类型独有的,其中Record Offset用给出要修改的记录在Page中的位置偏移,Update Field Count说明记录里有几个Field要修改,紧接着对每个Field给出了Field编号(Field Number),数据长度(Field Data Length)以及数据(Filed Data)。

2、作用于Space的REDO

这类REDO针对一个Space文件的修改,如MLOG_FILE_CREATE,MLOG_FILE_DELETE,MLOG_FILE_RENAME分别对应对一个Space的创建,删除以及重命名。由于文件操作的REDO是在文件操作结束后才记录的,因此在恢复的过程中看到这类日志时,说明文件操作已经成功,因此在恢复过程中大多只是做对文件状态的检查,以MLOG_FILE_CREATE来看看其中记录的内容:

同样的前三个字段还是Type,Space ID和Page Number,由于是针对Page的操作,这里的Page Number永远是0。在此之后记录了创建的文件flag以及文件名,用作重启恢复时的检查。

3、提供额外信息的Logic REDO

除了上述类型外,还有少数的几个REDO类型不涉及具体的数据修改,只是为了记录一些需要的信息,比如最常见的MLOG_MULTI_REC_END就是为了标识一个REDO组,也就是一个完整的原子操作的结束。

4、REDO是如何组织的

所谓REDO的组织方式,就是如何把需要的REDO内容记录到磁盘文件中,以方便高效的REDO写入,读取,恢复以及清理。我们这里把REDO从上到下分为三层:逻辑REDO层、物理REDO层和文件层。

1 逻辑REDO层

这一层是真正的REDO内容,REDO由多个不同Type的多个REDO记录收尾相连组成,有全局唯一的递增的偏移sn,InnoDB会在全局log_sys中维护当前sn的最大值,并在每次写入数据时将sn增加REDO内容长度。如下图所示:

2 物理REDO层

磁盘是块设备,InnoDB中也用Block的概念来读写数据,一个Block的长度OS_FILE_LOG_BLOCK_SIZE等于磁盘扇区的大小512B,每次IO读写的最小单位都是一个Block。除了REDO数据以外,Block中还需要一些额外的信息,下图所示一个Log Block的的组成,包括12字节的Block Header:前4字节中Flush Flag占用最高位bit,标识一次IO的第一个Block,剩下的31个个bit是Block编号;之后是2字节的数据长度,取值在[12,508];紧接着2字节的First Record Offset用来指向Block中第一个REDO组的开始,这个值的存在使得我们对任何一个Block都可以找到一个合法的的REDO开始位置;最后的4字节Checkpoint Number记录写Block时的next_checkpoint_number,用来发现文件的循环使用,这个会在文件层详细讲解。Block末尾是4字节的Block Tailer,记录当前Block的Checksum,通过这个值,读取Log时可以明确Block数据有没有被完整写完。

Block中剩余的中间498个字节就是REDO真正内容的存放位置,也就是我们上面说的逻辑REDO。我们现在将逻辑REDO放到物理REDO空间中,由于Block内的空间固定,而REDO长度不定,因此可能一个Block中有多个REDO,也可能一个REDO被拆分到多个Block中,如下图所示,棕色和红色分别代表Block Header和Tailer,中间的REDO记录由于前一个Block剩余空间不足,而被拆分在连续的两个Block中。

由于增加了Block Header和Tailer的字节开销,在物理REDO空间中用LSN来标识偏移,可以看出LSN和SN之间有简单的换算关系:

1
2
3
4
arduino复制代码constexpr inline lsn_t log_translate_sn_to_lsn(lsn_t sn) {
return (sn / LOG_BLOCK_DATA_SIZE * OS_FILE_LOG_BLOCK_SIZE +
sn % LOG_BLOCK_DATA_SIZE + LOG_BLOCK_HDR_SIZE);
}

SN加上之前所有的Block的Header以及Tailer的长度就可以换算到对应的LSN,反之亦然。

3 文件层

最终REDO会被写入到REDO日志文件中,以ib_logfile0、ib_logfile1…命名,为了避免创建文件及初始化空间带来的开销,InooDB的REDO文件会循环使用,通过参数innodb_log_files_in_group可以指定REDO文件的个数。多个文件收尾相连顺序写入REDO内容。每个文件以Block为单位划分,每个文件的开头固定预留4个Block来记录一些额外的信息,其中第一个Block称为Header Block,之后的3个Block在0号文件上用来存储Checkpoint信息,而在其他文件上留空:

其中第一个Header Block的数据区域记录了一些文件信息,如下图所示,4字节的Formate字段记录Log的版本,不同版本的LOG,会有REDO类型的增减,这个信息是8.0开始才加入的;8字节的Start LSN标识当前文件开始LSN,通过这个信息可以将文件的offset与对应的lsn对应起来;最后是最长32位的Creator信息,正常情况下会记录MySQL的版本。

现在我们将REDO放到文件空间中,如下图所示,逻辑REDO是真正需要的数据,用sn索引,逻辑REDO按固定大小的Block组织,并添加Block的头尾信息形成物理REDO,以lsn索引,这些Block又会放到循环使用的文件空间中的某一位置,文件中用offset索引:

虽然通过LSN可以唯一标识一个REDO位置,但最终对REDO的读写还需要转换到对文件的读写IO,这个时候就需要表示文件空间的offset,他们之间的换算方式如下:

1
2
c复制代码const auto real_offset =
log.current_file_real_offset + (lsn - log.current_file_lsn);

切换文件时会在内存中更新当前文件开头的文件offset,current_file_real_offset,以及对应的LSN,current_file_lsn,通过这两个值可以方便地用上面的方式将LSN转化为文件offset。注意这里的offset是相当于整个REDO文件空间而言的,由于InnoDB中读写文件的space层实现支持多个文件,因此,可以将首位相连的多个REDO文件看成一个大文件,那么这里的offset就是这个大文件中的偏移。

五 如何高效地写REDO

作为维护数据库正确性的重要信息,REDO日志必须在事务提交前保证落盘,否则一旦断电将会有数据丢失的可能,因此从REDO生成到最终落盘的完整过程成为数据库写入的关键路径,其效率也直接决定了数据库的写入性能。这个过程包括REDO内容的产生,REDO写入InnoDB Log Buffer,从InnoDB Log Buffer写入操作系统Page Cache,以及REDO刷盘,之后还需要唤醒等待的用户线程完成Commit。下面就通过这几个阶段来看看InnoDB如何在高并发的情况下还能高效地完成写REDO。

1 REDO产生

我们知道事务在写入数据的时候会产生REDO,一次原子的操作可能会包含多条REDO记录,这些REDO可能是访问同一Page的不同位置,也可能是访问不同的Page(如Btree节点分裂)。InnoDB有一套完整的机制来保证涉及一次原子操作的多条REDO记录原子,即恢复的时候要么全部重放,要不全部不重放,这部分将在之后介绍恢复逻辑的时候详细介绍,本文只涉及其中最基本的要求,就是这些REDO必须连续。InnoDB中通过min-transaction实现,简称mtr,需要原子操作时,调用mtr_start生成一个mtr,mtr中会维护一个动态增长的m_log,这是一个动态分配的内存空间,将这个原子操作需要写的所有REDO先写到这个m_log中,当原子操作结束后,调用mtr_commit将m_log中的数据拷贝到InnoDB的Log Buffer。

2 写入InnoDB Log Buffer

高并发的环境中,会同时有非常多的min-transaction(mtr)需要拷贝数据到Log Buffer,如果通过锁互斥,那么毫无疑问这里将成为明显的性能瓶颈。为此,从MySQL 8.0开始,设计了一套无锁的写log机制,其核心思路是允许不同的mtr,同时并发地写Log Buffer的不同位置。不同的mtr会首先调用log_buffer_reserve函数,这个函数里会用自己的REDO长度,原子地对全局偏移log.sn做fetch_add,得到自己在Log Buffer中独享的空间。之后不同mtr并行的将自己的m_log中的数据拷贝到各自独享的空间内。

1
2
c复制代码/* Reserve space in sequence of data bytes: */
const sn_t start_sn = log.sn.fetch_add(len);

3 写入Page Cache

写入到Log Buffer中的REDO数据需要进一步写入操作系统的Page Cache,InnoDB中有单独的log_writer来做这件事情。这里有个问题,由于Log Buffer中的数据是不同mtr并发写入的,这个过程中Log Buffer中是有空洞的,因此log_writer需要感知当前Log Buffer中连续日志的末尾,将连续日志通过pwrite系统调用写入操作系统Page Cache。整个过程中应尽可能不影响后续mtr进行数据拷贝,InnoDB在这里引入一个叫做link_buf的数据结构,如下图所示:

link_buf是一个循环使用的数组,对每个lsn取模可以得到其在link_buf上的一个槽位,在这个槽位中记录REDO长度。另外一个线程从开始遍历这个link_buf,通过槽位中的长度可以找到这条REDO的结尾位置,一直遍历到下一位置为0的位置,可以认为之后的REDO有空洞,而之前已经连续,这个位置叫做link_buf的tail。下面看看log_writer和众多mtr是如何利用这个link_buf数据结构的。这里的这个link_buf为log.recent_written,如下图所示:

图中上半部分是REDO日志示意图,write_lsn是当前log_writer已经写入到Page Cache中日志末尾,current_lsn是当前已经分配给mtr的的最大lsn位置,而buf_ready_for_write_lsn是当前log_writer找到的Log Buffer中已经连续的日志结尾,从write_lsn到buf_ready_for_write_lsn是下一次log_writer可以连续调用pwrite写入Page Cache的范围,而从buf_ready_for_write_lsn到current_lsn是当前mtr正在并发写Log Buffer的范围。下面的连续方格便是log.recent_written的数据结构,可以看出由于中间的两个全零的空洞导致buf_ready_for_write_lsn无法继续推进,接下来,假如reserve到中间第一个空洞的mtr也完成了写Log Buffer,并更新了log.recent_written*,如下图:

这时,log_writer从当前的buf_ready_for_write_lsn向后遍历log.recent_written,发现这段已经连续:

因此提升当前的buf_ready_for_write_lsn,并将log.recent_written的tail位置向前滑动,之后的位置清零,供之后循环复用:

紧接log_writer将连续的内容刷盘并提升write_lsn。

4 刷盘

log_writer提升write_lsn之后会通知log_flusher线程,log_flusher线程会调用fsync将REDO刷盘,至此完成了REDO完整的写入过程。

5 唤醒用户线程

为了保证数据正确,只有REDO写完后事务才可以commit,因此在REDO写入的过程中,大量的用户线程会block等待,直到自己的最后一条日志结束写入。默认情况下innodb_flush_log_at_trx_commit = 1,需要等REDO完成刷盘,这也是最安全的方式。当然,也可以通过设置innodb_flush_log_at_trx_commit = 2,这样,只要REDO写入Page Cache就认为完成了写入,极端情况下,掉电可能导致数据丢失。

大量的用户线程调用log_write_up_to等待在自己的lsn位置,为了避免大量无效的唤醒,InnoDB将阻塞的条件变量拆分为多个,log_write_up_to根据自己需要等待的lsn所在的block取模对应到不同的条件变量上去。同时,为了避免大量的唤醒工作影响log_writer或log_flusher线程,InnoDB中引入了两个专门负责唤醒用户的线程:log_wirte_notifier和log_flush_notifier,当超过一个条件变量需要被唤醒时,log_writer和log_flusher会通知这两个线程完成唤醒工作。下图是整个过程的示意图:

多个线程通过一些内部数据结构的辅助,完成了高效的从REDO产生,到REDO写盘,再到唤醒用户线程的流程,下面是整个这个过程的时序图:

六 如何安全地清除REDO

由于REDO文件空间有限,同时为了尽量减少恢复时需要重放的REDO,InnoDB引入log_checkpointer线程周期性的打Checkpoint。重启恢复的时候,只需要从最新的Checkpoint开始回放后边的REDO,因此Checkpoint之前的REDO就可以删除或被复用。

我们知道REDO的作用是避免只写了内存的数据由于故障丢失,那么打Checkpiont的位置就必须保证之前所有REDO所产生的内存脏页都已经刷盘。最直接的,可以从Buffer Pool中获得当前所有脏页对应的最小REDO LSN:lwm_lsn。但光有这个还不够,因为有一部分min-transaction的REDO对应的Page还没有来的及加入到Buffer Pool的脏页中去,如果checkpoint打到这些REDO的后边,一旦这时发生故障恢复,这部分数据将丢失,因此还需要知道当前已经加入到Buffer Pool的REDO lsn位置:dpa_lsn。取二者的较小值作为最终checkpoint的位置,其核心逻辑如下:

1
2
3
4
5
6
7
c复制代码/* LWM lsn for unflushed dirty pages in Buffer Pool */
lsn_t lwm_lsn = buf_pool_get_oldest_modification_lwm();

/* Note lsn up to which all dirty pages have already been added into Buffer Pool */
const lsn_t dpa_lsn = log_buffer_dirty_pages_added_up_to_lsn(log);

lsn_t checkpoint_lsn = std::min(lwm_lsn, dpa_lsn);

MySQL 8.0中为了能够让mtr之间更大程度的并发,允许并发地给Buffer Pool注册脏页。类似与log.recent_written和log_writer,这里引入一个叫做recent_closed的link_buf来处理并发带来的空洞,由单独的线程log_closer来提升recent_closed的tail,也就是当前连续加入Buffer Pool脏页的最大LSN,这个值也就是上面提到的dpa_lsn。需要注意的是,由于这种乱序的存在,lwm_lsn的值并不能简单的获取当前Buffer Pool中的最老的脏页的LSN,保守起见,还需要减掉一个recent_closed的容量大小,也就是最大的乱序范围,简化后的代码如下:

1
2
3
4
5
6
7
8
9
c复制代码/* LWM lsn for unflushed dirty pages in Buffer Pool */
const lsn_t lsn = buf_pool_get_oldest_modification_approx();
const lsn_t lag = log.recent_closed.capacity();
lsn_t lwm_lsn = lsn - lag;

/* Note lsn up to which all dirty pages have already been added into Buffer Pool */
const lsn_t dpa_lsn = log_buffer_dirty_pages_added_up_to_lsn(log);

lsn_t checkpoint_lsn = std::min(lwm_lsn, dpa_lsn);

这里有一个问题,由于lwm_lsn已经减去了recent_closed的capacity,因此理论上这个值一定是小于dpa_lsn的。那么再去比较lwm_lsn和dpa_lsn来获取Checkpoint位置或许是没有意义的。

上面已经提到,ib_logfile0文件的前三个Block有两个被预留作为Checkpoint Block,这两个Block会在打Checkpiont的时候交替使用,这样来避免写Checkpoint过程中的崩溃导致没有可用的Checkpoint。Checkpoint Block中的内容如下:

首先8个字节的Checkpoint Number,通过比较这个值可以判断哪个是最新的Checkpiont记录,之后8字节的Checkpoint LSN为打Checkpoint的REDO位置,恢复时会从这个位置开始重放后边的REDO。之后8个字节的Checkpoint Offset,将Checkpoint LSN与文件空间的偏移对应起来。最后8字节是前面提到的Log Buffer的长度,这个值目前在恢复过程并没有使用。

七 总结

本文系统的介绍了InnoDB中REDO的作用、特性、组织结构、写入方式已经清理时机,基本覆盖了REDO的大多数内容。关于重启恢复时如何使用REDO将数据库恢复到正确的状态,将在之后介绍InnoDB故障恢复机制的时候详细介绍。

参考

[1] MySQL 8.0.11Source Code Documentation: Format of redo log

dev.mysql.com/doc/dev/mys…

[2] MySQL 8.0: New Lock free, scalable WAL design

mysqlserverteam.com/mysql-8-0-n…

[3] How InnoDB handles REDO logging

www.percona.com/blog/2011/0…

[4] MySQL Source Code

github.com/mysql/mysql…

[5] 数据库故障恢复机制的前世今生

catkang.github.io/2019/01/16/…

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

什么?还在用delete删除数据《死磕MySQL系列 九》

发表于 2021-11-12

系列文章

五、如何选择普通索引和唯一索引《死磕MySQL系列 五》

六、五分钟,让你明白MySQL是怎么选择索引《死磕MySQL系列 六》

七、字符串可以这样加索引,你知吗?《死磕MySQL系列 七》

八、无法复现的“慢”SQL《死磕MySQL系列 八》

参与了好几个项目开发,每个项目随着业务量的增大,MySQL数据日益剧增,例如其中一个项目中得用户足迹表,那是非常的疯狂,只怪我大意了,没有闪。

这篇文章我会从delete对性能的影响,以及如何以正确的姿势来删除数据。

在MySQL中Innodb存储引擎的表存在两部分,一部分是表结构,另一部分是表数据。

在MySQL8.0之前/var/lib/mysql下都会存在.frm文件,在MySQL8.0之后就不存在了。这是因为MySQL8.0中已经允许把表结构定义放到数据字典中了,是用参数innodb_file_per_table来决定的。

一、表空间

表空间分为几种,系统表空间、用户表空间、undo空间。

系统表空间:MySQL内部的数据字典,如information_schema库下的数据。

用户表空间:自己建立的表结构数据

undo空间:存储Undo信息,用于快速回滚。

MySQL8.0之前表结构是在系统表空间存储的,在MySQL5.6.6后可以使用参数innodb_file_per_table来控制。

设置为off时,表数据是放在系统表空间中,也就是MySQL的数据字典放在一起。

设置为on时,innodb存储引擎的表数据存储在.idb文件中。

你知道表定义存储在哪里吗?

来到死磕MySQL系列的专用数据库kaka,新建一张表evt_sms。

猜一下创建的evt_sms表结构定义存储在哪里呢?

在information_schema库里边的TABLES中,执行查询SELECT TABLE_NAME,TABLE_COMMENT FROM TABLES WHERE TABLE_TYPE='BASE TABLE';

我们自定义的表类型是TABLE_TYPE。

说了这么是为了解释如果把innodb_file_per_table设置为off,则表数据也会存放在这里。

问题:如果数据存在放共享表空间中,表删除了,空间会删除吗?

答案是不会的。

参数innodb_file_per_table设置为on数据存储在哪里呢?

一般情况下是在var/lib/mysql中,会看到你创建的数据库,进入到数据库中就能看到一张表对应一个ibd文件。

数据就是存储在这里。

结论

在项目开始阶段,切记将innodb_file_per_table设置为on,这是正确的做法。

二、数据删除流程

现在你应该知道Innodb存储引擎用的是B+树数据结构,如下图。

如果现在删了主键ID为4的这条记录,Innodb引擎会把ID为4的这条记录标记为删除,如果之后再插入ID为4的记录,可能会复用这个位置,但磁盘文件大小并不会缩小。

隐式字段

这里就牵扯到了mvcc中的一个知识点,MVCC实现原理是由俩个隐式字段、undo日志、Read view来实现的。

上文说的标记删除就是隐式字段中的delete flag,即记录被更新或删除,这里的删除并不代表真的删除,而是将这条记录的delete flag改为true。

在MVCC:听说有人好奇我的底层实现这篇文章中也给大家留下了一个伏笔,数据库的删除是真的删除吗?

问题:删了一个数据页的所有数据会怎么样

跟单条数据是一样的,整个数据页都是可以复用的。

记录的复用是仅限于符合范围条件的数据,例如上文删除的ID为4这条记录,如果在插入ID为4就会复用。

这里需要给大家再聊一个新的知识点页合并,若相邻的两个数据页利用率都很低,系统就会把这两个数据页合并到一个页上,另一个数据页就会标记为可复用。

问题:使用delete把整个表的数据都删除了会怎么样

答案是,所有的数据页都会标记为可复用,但是磁盘文件大小是不会改变的。

三、实践全表删除表文件大小不改变

经过添加数据后表数据已经达到近100W了,文件大小已经达到108M。

扩展

这里大家应该能看见stopped,就是执行命令ctrl + z来的,作用是开始我们在MySQL窗口里边,但不想退出MySQL窗口查看MySQL表文件大小,然后就可以执行这个命令结束任务。

查看完后可以在执行fg返回到MySQL窗口。

问题:Linux如何把文件单位显示为M

假设刚刚直接执行ll命令查看文件,那么就需要手动计算文件大小,很不方便。

执行ll -h命令则可以直观的看到文件大小。

删除数据查看磁盘文件是否缩小

为了直观看大文件大小变化,咔咔直接把表里边的数据全部删了,再看文件大小,还是108M。文件大小是没有变化的。

四、如何正确的减少磁盘文件

在第三小节中,我们演示了删除了100W数据后文件大小是没有改变的,也就是空洞问题影响的,接下来就解决这种问题。

问题:空洞是如何产生的?

到了这里都应该知道空洞是因为大量的增删改造成的。

解决思路

你可以新建一个evt_sms_copy表,然后根据主键ID递增的顺序,把数据从evt_sms读入evt_sms1中。

这样就可以达到因为空洞造成的磁盘文件大小无法收缩问题。

问题:为什么能解决呢?

因为evt_sms_copy是一张新的表,并且数据是以主键ID递增的,索引是紧促的,数据页利用率已经达到了最高峰状态,这样就起到了磁盘文件无法收缩问题。

上干货

直接执行alter table evt_sms engine = Innodb 命令来达到磁盘文件收缩。

这里需要跟大家聊一下不同版本处理不同。

在MySQL5.5之前,这个命令做的事情跟我们解决思路是一样的,不同的是evt_sms_copy是不用自己创建的。

在执行命令期间如有新增数据的话,会造成数据丢失,因为在MySQL5.5之前版本的DDL不是Online的。因此不能有数据的改动。

现在MySQL都已经更新到8版本了,如果你是新项目就直接用8版本,不要在用5.6以前的老版本了,咔咔在18年开始就已经在使用MySQL8.0版本了。

在锁那一期文章中跟大家聊了MySQL5.6在DDL操作做了优化,引入了Online DDL。

优化后的执行流程

  • 建立临时文件tmp_file,把表的B+树存储到临时文件中。若此时有对表的操作,则会记录在row log文件中。
  • 把数据从原表全部刷到临时文件后,此时临时文件的数据就跟原表的数据一致。
  • 最后用临时文件替换表A的数据文件。

Online DDL的由来

可以看到在收缩磁盘文件时有数据更新会记录在row log中,意思就是在收缩磁盘空间时是可以对表进行增删改查的。

注意点

在进行磁盘文件收缩的过程中,都会全表扫描原数据和新增临时文件,如果你的表非常大,会非常消耗IO和CPU。

因此,你要安全的做这个操作,可以使用开源的gh-ost来进行。

结论

当你想收缩因为大量增删改查而导致表磁盘文件非常大时就可以执行alter table evt_sms engine=Innodb命令来达到收缩表空间的目的。

五、实践是检验认识是否具有真理性的唯一标准

都应该知道实践是检验认识是否具有真理性的唯一标准,那么接下里就对本文提出的结论进行实际操作一下。

  • 先执行ctrl + z结束MySQL任务窗口
  • 执行ll -h查看此时表evt_sms磁盘文件大小为108M
  • 执行fg返回到MySQL任务窗口
  • 执行命令alter table evt_sms engine=Innodb
  • 再执行ctrl + z,执行ll -h查看磁盘文件大小已经到了128k。

上图即是咔咔操作的全过程,得到的结论就是执行命令alter table ect_sms engine = Innodb可以收缩由于大量增删改查的表引发的空洞问题。最终达到收缩表空间目的。

六、开发建议

删除数据不要使用delete,而是使用软删除,做一个标记删除即可。

这样既不会出现空洞问题,也方便数据溯源。

每张表必备三个字段create_time、update_time、delete_time。

七、总结

通过本期文章我们需要知道以下几点。

  • 通过大量增删改查的表会出现空洞
  • 干掉空洞需要执行alter table evt_sms engine=Innodb来解决
  • 使用delete删除数据只会做一个标记处理,并不会真正删除空间
  • 本文所有的结论都基于innodb_file_per_table = on

“

坚持学习、坚持写作、坚持分享是咔咔从业以来所秉持的信念。愿文章在偌大的互联网上能给你带来一点帮助,我是咔咔,下期见。

”

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RabbitMQ(5) 死信队列的场景演示

发表于 2021-11-12

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

一、死信队列

死信,在官网中对应的单词为“Dead Letter”,它是 RabbitMQ 的一种消息机制。

​ 一般来说,生产者将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,如果它一直无法消费某条数据,那么可以把这条消息放入死信队列里面。等待条件满足了再从死信队列中取出来再次消费,从而避免消息丢失。

死信消息来源:

  • 消息 TTL 过期
  • 队列满了,无法再次添加数据
  • 消息被拒绝(reject 或 nack),并且 requeue =false

二、死信实战(基础版)

在这种,我们使用基础版来完成死信队列的几种情况的演示。

定义一个生产者 向 普通交换机下的普通队列发送消息。

定义一个消费者向普通交换机下的普通队列消费消息,当它消息满足三大条件之一时,消息就发送到死信交换机下的死信队列。

死信交换机也不是什么特殊交换机,是自己命名的,只是专门用来接收死信消息。

2.1 发送 TTL 消息

消费者1:消费普通消息,如果普通消息它过期了,就将它转发到死信队列中。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
java复制代码public class Consumer1 {
// 普通交换机
public static final String NORML_EXCHANGE ="normal_exchange";
// 死信交换机
public static final String DEAD_EXCHANGE ="dead_exchange";
// 普通队列的名称
public static final String NORML_QUEUE ="normal_queue";
// 死信队列的名称
public static final String DEAD_QUEUE ="dead_queue";

public static void main(String[] args) throws IOException, TimeoutException {
// 1、创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2、设置连接属性
connectionFactory.setHost("192.168.81.102");
connectionFactory.setPort(5672);
connectionFactory.setUsername("test");
connectionFactory.setPassword("test");
connectionFactory.setVirtualHost("test");

//3、从连接工厂中获取连接
Connection connection = connectionFactory.newConnection("consumer2");
//4、从连接中获取通道 channel
Channel channel = connection.createChannel();

DeliverCallback deliverCallback =(String a, Delivery b)->{
String message = new String(b.getBody());
System.out.println("work1:"+message);
// false 表示只确认 b.DelivertTag 这条消息,true 表示确认 小于等于 b.DelivertTag 的所有消息
channel.basicAck(b.getEnvelope().getDeliveryTag(),false);
};
CancelCallback cancelCallback =(String a)->{
System.out.println("消息消费被中断");
};
// 5、声明死信和普通交换机,类型为 direct
channel.exchangeDeclare(NORML_EXCHANGE,BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);
// 队列声明:普通队列需要设置 arguments 参数
HashMap<String, Object> arguments = new HashMap<>();
// 过期时间,正常队列设置死信交换机,当消费失败就发送到死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
arguments.put("x-dead-letter-routing-key","dead");// 设置路由key
// 6、队列声明:普通队列需要设置 arguments 参数,普通队列添加参数
channel.queueDeclare(NORML_QUEUE,false,false,false,arguments);
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
// 7、绑定关系
channel.queueBind(NORML_QUEUE,NORML_EXCHANGE,"normal");
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"dead");

// 8、消费消息
/**
* 消费者消费消息
* @params1: 消费哪个队列
* @params2:消费成功之后是否要自动应答 true代表自动应答 ,flase代表手动应答。
* @params3: 消费者消费成功的回调
* @params4: 消费者消费失败的回调
*/
channel.basicConsume(NORML_QUEUE,false,deliverCallback,cancelCallback);
}
}

生产者:

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
java复制代码public class Producer {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// 1、创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2、设置连接属性
connectionFactory.setHost("192.168.81.102");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("test");
connectionFactory.setUsername("test");
connectionFactory.setPassword("test");

//3、从连接工厂中获取连接
Connection connection = connectionFactory.newConnection("producer");
//4、从连接中获取通道 channel
Channel channel = connection.createChannel();
AMQP.BasicProperties properties =new AMQP.BasicProperties().builder().expiration("10000").build(); // 单位毫秒 ,这里是10s
// 6、发送死信队列,设置 TTL 消息
for (int i = 1; i <=20; i++) {
// @params1: 交换机exchange
// @params2: 队列名称/routing
// @params3: 属性配置,这里添加 ttl 消息
// @params4: 发送消息的内容
channel.basicPublish("normal_exchange","normal",properties,("潇雷挺帅,说第"+i+"遍。").getBytes());
System.out.println("发送第"+i);
Thread.sleep(1000);
}
}
}

先启动 消费者1 然后创建了声明。再关闭消费者1,开始发送带有 ttl 的消息。最后可以发现,20条消息全都被发送到死信队列里面了。

image-20211112100711099

image-20211112100758753

image-20211112100809713

2.2 队列满了

消费者1添加以下代码,添加正常队列的长度限制。当消息超过 5条之后,再过来的消息就会变成死信队列。

1
javascript复制代码 arguments.put("x-max-length",5);

实现的效果如下:

image-20211112102131585

2.3 消息被拒

1
2
3
4
5
6
7
8
9
10
vbnet复制代码   DeliverCallback deliverCallback =(String a, Delivery b)->{
String message = new String(b.getBody());
System.out.println("work1:"+message);
if(message.equals("5")){
System.out.println("该消息被拒绝");
channel.basicReject(b.getEnvelope().getDeliveryTag(),false);//true 塞回原队列,false 代表不放回原队列。
}else{
channel.basicAck(b.getEnvelope().getDeliveryTag(),false);
}
};

三、死信实战(SpringBoot 版)

在这小节种,通过 Springboot 项目再次熟悉下这三大场景。

业务流程:

  • 1、正常业务消息被投递到正常业务的 Exchange,该 Exchange 根据路由键将消息路由绑定到正常队列
  • 2、正常的消息变成死信消息之后,会被自动投递到该队列绑定的死信交换机上;
  • 3、死信交换机收到消息后,将消息根据路由规则路由到指定的死信队列
  • 4、消息到达死信队列后,可监听该死信队列,处理死信消息。

3.1 消息被拒

1、配置死信队列config

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
typescript复制代码@Configuration
public class DeadConfig {
// 交换机
public static final String dead_exchange_name = "dead_exchange_springboot";
// 普通交换机
public static final String normal_exchange_name = "normal_exchange_springboot";
// 普通队列
public static final String normal_queue_name="normal_queue_springboot";
// 死信队列
public static final String dead_queue_name = "dead_queue_springboot";

// 声明交换机
@Bean("deadExchange")
public DirectExchange deadExchange(){
return new DirectExchange(dead_exchange_name);
}
// 声明交换机
@Bean("normalExchange")
public DirectExchange normalExchange(){
return new DirectExchange(normal_exchange_name);
}
// 声明队列
@Bean("deadQueue")
public Queue deadQueue() {
return QueueBuilder.durable(dead_queue_name).build();
}
// 声明队列
@Bean("normal_queue_name")
public Queue normalQueue() {
return QueueBuilder.durable(normal_queue_name)
.withArgument("x-dead-letter-exchange",dead_exchange_name)
.withArgument("x-dead-letter-routing-key","dead")
.build();
}
// 绑定队列到交换机
@Bean
public Binding queueBingExchange3(){
return BindingBuilder.bind(normalQueue()).to(normalExchange()).with("normal");
}
// 绑定队列到交换机
@Bean
public Binding queueBingExchange4(){
return BindingBuilder.bind(deadQueue()).to(deadExchange()).with("dead");
}
}

2、生产 yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码# 服务端口
server:
port: 8300
# 配置rabbitmq服务
spring:
rabbitmq:
username: test
password: test
virtual-host: test
host: 192.168.81.102
port: 5672
template:
# 消息路由失败通知监听者,而不是将消息丢弃
mandatory: true
publisher-confirm-type: correlated
publisher-returns: true

3、生产者发送消息

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@Slf4j
@RestController
@RequestMapping("/msg")
public class SendController {
@Autowired
private RabbitTemplate rabbitTemplate;

@GetMapping("/dead/{message}")
public void sendConfirmMsg(@PathVariable String message){
rabbitTemplate.convertAndSend(DeadConfig.normal_exchange_name,"normal",message,new CorrelationData("1"));
}
}

4、消费者 yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
yaml复制代码# 服务端口
server:
port: 8301
# 配置rabbitmq服务
spring:
rabbitmq:
username: test
password: test
virtual-host: test
host: 192.168.81.102
port: 5672
listener:
direct:
# 表示消费者消费成功消息以后需要手动的进行签收(ack)
acknowledge-mode: manual
# 每次处理一个消息
prefetch: 1
retry:
# 开启重试消费
enabled: true
# 最大重试次数
max-attempts: 5

5、消费者

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码    @RabbitListener(queues = DeadConfig.normal_queue_name)
public void consumerDead(Message message, Channel channel) throws IOException {
String s = new String(message.getBody());
System.out.println(s);
if(s.equals("2")){
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); //basicAck:表示成功确认,使用此回执方法后,消息会被rabbitmq broker 删除。
log.info("接收的消息为:{}",message);
}else{
channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
log.info("未消费数据");
}
}

在浏览器发送消息之后,被拒绝的消息进入到 死信队列中。

image-20211112114924584

3.2 队列满了

在普通队列中添加 x-max-length

image-20211112135236184

然后将之前的队列删除,重新启动完生产者和消费者,生成新的队列,关闭消费者后,让生产者发送消息,这时候没有消费者,队列在两条的时候会满了,然后多余的信息会发送到死信队列里面。如图所示:

image-20211112135800720

该队列的详情信息也可以点进去看:

image-20211112135834242

3.3 发送 TTL 消息

设置队列消息的过期时间 x-message-ttl ,即该时间到了之后该条消息就会被发送到 死信队列中。

先清除之前的队列,然后配置这个参数,设为 5秒

image-20211112140038929

在发送完消息,该条消息在该队列中待了5秒之后进入死信队列中

image-20211112140257104

总结

死信队列可以实现消息在未被正常消费的场景下,对这些消息进行其他处理,保证消息不会丢失。

这篇文章还是入门级别的对这死信队列做了不同场景的演示,这种业务场景是要求消息必须可靠的,通过死信队列为这些队列的消息的可靠性提供保障。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

动态规划攻略之:比特位计数

发表于 2021-11-12

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」

题目

给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。

示例1:

1
2
3
4
5
6
ini复制代码输入:n = 2
输出:[0,1,1]
解释:
0 --> 0
1 --> 1
2 --> 10

示例2:

1
2
3
4
5
6
7
8
9
lua复制代码输入:n = 5
输出:[0,1,1,2,1,2]
解释:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101

解题思路

首先来看一个概念:最低设置位

最低设置位是正整数二进制中最低的 1 所在位。例如,1010 的二进制表示是 1010,其最低设置位为 2。

于是,定义一个正整数 x, 令 y = x & (x - 1), 则 y 为将 x 的最低设置位从 1 变成 0 之后的数,显然 0 <= y < x, bits[x] = bits[y] + 1。因此对于任意正整数 x, 都有 bits[x] = bits[x & (x - 1)] + 1。遍历从 1 到 n 的每个正整数,计算 bits 的值。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码class Solution {
public int[] countBits(int n) {
int[] bits = new int[n + 1];
int highBit = 0;
for (int i = 1; i <= n; i++) {
if ((i & (i - 1)) == 0) {
highBit = i;
}
bits[i] = bits[i - highBit] + 1;
}
return bits;
}
}

最后

  • 时间复杂度:O(n)。
  • 空间复杂度:O(1)。

往期文章:

  • 二叉树总结:二叉树的属性
  • 二叉树总结:二叉树的修改与构造
  • StoreKit2 有这么香?嗯,我试过了,真香
  • 看完这篇文章,再也不怕面试官问我如何构造二叉树啦!
  • 那帮做游戏的又想让大家氪金,太坏了!
  • 手把手带你撸一个网易云音乐首页 | 适配篇
  • 手把手带你撸一个网易云音乐首页(三)
  • 手把手带你撸一个网易云音乐首页(二)
  • 手把手带你撸一个网易云音乐首页(一)
  • 代码要写注释吗?写你就输了
  • Codable发布这么久我就不学,摸鱼爽歪歪,哎~就是玩儿
  • iOS 优雅的处理网络数据,你真的会吗?不如看看这篇
  • UICollectionView 自定义布局!看这篇就够了

请你喝杯 ☕️ 点赞 + 关注哦~

  1. 阅读完记得给我点个赞哦,有👍 有动力
  2. 关注公众号— HelloWorld杰少,第一时间推送新姿势

最后,创作不易,如果对大家有所帮助,希望大家点赞支持,有什么问题也可以在评论区里讨论😄~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

C++11 / C++14 中 tuple 的高级使用 前置

发表于 2021-11-12

「这是我参与11月更文挑战的第 10 天,活动详情查看:2021最后一次更文挑战」。

参加该活动的第 23 篇文章

前置知识

tuple 和 vector 比较

vector 只能存储同一种类型的数据,tuple 可以存储任意类型的数据;

vector 和 variant 比较

二者都可以存储不同类型的数据,但是 variant 的类型个数是固定的,而 tuple 的类型个数不是固定的,是变长的,更为强大

std::decay

类模板说明

为类型 T 应用从左值到右值(lvalue-to-rvalue) 、数组到指针(array-to-pointer) 和函数到指针(function-to-pointer) 的隐式转换。

转换将移除类型 T 的 cv 限定符(const 和 volatile 限定符),并定义结果类型为成员 decay<T>::type 的类型。这种转换很类似于当函数的所有参数按值传递时发生转换。

  • 如果类型 T 是一个函数类型,那么从函数到指针的类型转换将被应用,并且 T 的衰变类型等同于: add_pointer<T>::type
  • 如果类型 T 是一个数组类型,那么从数组到指针的类型转换将被应用,并且 T 的衰变类型等同于:
    add_pointer<remove_extent<remove_reference<T>::type>::type>::type
  • 当左值到右值转换被应用时,T 的衰变类型等同于:remove_cv<remove_reference<T>::type>::type

模板参数说明

T 表示某种类型。

  • 当 T 是引用类型,decay<T>::type 返回 T 引用的元素类型;
  • 当 T 是非引用类型,decay<T>::type 返回 T 的类型

正文

前文介绍过 C++11 / C++14 中 tuple 的基本使用,接下来将介绍 tuple 的一些高级用法

获取 tuple 中某个位置元素的类型

其中我们通过 std::tuple_element 来获取 tuple 元素的类型。
然后第几个元素就从 tuple std::get 第几个元素出来。

1
2
3
4
5
6
cpp复制代码template <typename Tuple>
void Fun(Tuple &tp)
{
std::tuple_element<0, Tuple>::type first = std::get<0>(mytuple);
std::tuple_element<1, Tuple>::type second = std::get<1>(mytuple);
}

获取 tuple 的元素个数

1
2
cpp复制代码tuple t;
int size = std::tuple_size<decltype(t))>::value;

遍历 tuple 中的每个元素

因为 tuple 的参数是变长的,而且类型也可能不同,并没有 for_each 函数,如果我们想遍历 tuple 中的每个元素,需要自己写代码实现。

举例,要打印 tuple 中的每个元素的话,代码如下

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
cpp复制代码template <class Tuple, std::size_t N>
struct TuplePrinter
{
static void print(const Tuple &t)
{
TuplePrinter<Tuple, N - 1>::print(t); ///< 递归
std::cout << ", " << std::get<N - 1>(t); ///< 第 N-1 个元素
}
};

template <class Tuple>
struct TuplePrinter<Tuple, 1> ///< 递归终止, N == 1
{
static void print(const Tuple &t)
{
std::cout << std::get<0>(t); ///< 第 0 个元素
}
};

/// @note 可变参模板函数
template <class... Args>
void PrintTuple(const std::tuple<Args...> &t)
{
std::cout << "(";
/// @note tuple 类型、参数个数
TuplePrinter<decltype(t), sizeof...(Args)>::print(t);
std::cout << ")\n";
}

根据 tuple 元素值获取其对应的索引

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
30
31
32
33
34
35
36
37
38
cpp复制代码namespace detail ///< 命名空间
{
template <int I, typename T, typename... Args>
struct find_index ///< 泛化版本
{
static int call(std::tuple<Args...> const &t, T &&val)
{
/// @note 如果找到该值,则返回对应索引,否则继续递归查找
return (std::get<I - 1>(t) == val) ? I - 1 : find_index<I - 1, T, Args...>::call(t, std::forward<T>(val));
}
};

template <typename T, typename... Args>
struct find_index<0, T, Args...> ///< 偏特化,递归终止
{
static int call(std::tuple<Args...> const &t, T &&val)
{
/// @note 如果找到该值,则返回第 0 个索引,否则返回 -1
return (std::get<0>(t) == val) ? 0 : -1;
}
};
}

/// @note 可变参模板函数
template <typename T, typename... Args>
int find_index(std::tuple<Args...> const &t, T &&val)
{
return detail::find_index<sizeof...(Args) - 1, T, Args...>::
call(t, std::forward<T>(val));
}

int main()
{
std::tuple<int, int, int, int> a(2, 3, 1, 4);
std::cout << find_index(a, 1) << std::endl; // Prints 2
std::cout << find_index(a, 2) << std::endl; // Prints 0
std::cout << find_index(a, 5) << std::endl; // Prints -1 (not found)
}

展开 tuple,并将其元素作为函数的参数

这样就可以用统一的形式来调用任意的函数及其参数,我们还能根据需要对 tuple 元素进行单独处理

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
cpp复制代码#include <tuple>
#include <type_traits>
#include <utility>

template <size_t N>
struct Apply ///< 泛化
{
template <typename F, typename T, typename... A>
static inline auto apply(F &&f, T &&t, A &&...a)
-> decltype(Apply<N - 1>::apply( ///< 推断返回值类型
std::forward<F>(f),
std::forward<T>(t),
std::get<N - 1>(std::forward<T>(t)),
std::forward<A>(a)...))
{
/// @note 递归
return Apply<N - 1>::apply(std::forward<F>(f),
std::forward<T>(t),
std::get<N - 1>(std::forward<T>(t)), ///< 取出并传递相应索引的 tuple 元素
std::forward<A>(a)...);
}
};

template <>
struct Apply<0> ///< 特化,终止递归
{
template <typename F, typename T, typename... A>
static inline auto apply(F &&f, T &&, A &&...a)
-> decltype(std::forward<F>(f)(std::forward<A>(a)...)) ///< 推断返回值类型
{
return std::forward<F>(f)(std::forward<A>(a)...);
}
};

template <typename F, typename T>
inline auto apply(F &&f, T &&t)
-> decltype(Apply<std::tuple_size< ///< 推断返回值类型
typename std::decay<T>::type>::value>::apply(std::forward<F>(f),
std::forward<T>(t)))
{
return Apply<std::tuple_size<
typename std::decay<T>::type>::value>::apply(std::forward<F>(f),
std::forward<T>(t));
}

/// @note 测试函数
void one(int i, double d)
{
std::cout << "function one(" << i << ", " << d << ");\n";
}
int two(int i)
{
std::cout << "function two(" << i << ");\n";
return i;
}

/// @note 测试代码
int main()
{
std::tuple<int, double> tup(23, 4.5);
apply(one, tup);

int d = apply(two, std::make_tuple(2));

return 0;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【设计模式系列】用代理模式避免rm -rf /*

发表于 2021-11-12

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

前言

也许你没有使用过Linux,但是一定听说过rm -rf /* 的传说,这个命令会将系统中所有的文件删除,直接导致操作系统无法使用,只能重装系统,如果在生产环境执行基本上是灾难。

rmrf.gif

那么实际管理过程中,肯定不会让开发人员具备能执行这么高权限的账号,只有运维人员或者运维老大才有这样的权限。今天我们就用设计模式中的来讲一讲,操作系统是如何实现对于不同用户来控制访问权限的。

代理模式定义

定义: 为另一个对象提供代理或占位符以控制对它的访问。

代理模式是一种结构型的设计模式,其作用主要是控制对一个已有服务或对象的访问。

定义本身非常清楚,当我们想要提供对功能的访问控制时,就会使用代理设计模式。

代理模式实现

在我们开头的例子中,操作系统中具备执行命令的功能,我们假设这个功能在一个对象中,如果我们想把这个功能开放给所有的用户(客户端),那可能会有严重的问题,因为客户端会发出命令来删除一些系统文件或更改一些你不想要的设置,比如执行rm -rf /*。

这种问题怎么解决呢?我们可以对这个具备执行命令能力的对象建立一个代理,在代理类中对于不同的用户进行访问权限控制。

代理模式主类

由于我们是根据接口编写Java代码的,下面是我们的接口及其实现类。

1
2
3
4
java复制代码public interface CommandExecutor {

public void runCommand(String cmd) throws Exception;
}

接口的具体实现:

1
2
3
4
5
6
7
8
9
10
java复制代码import java.io.IOException;

public class CommandExecutorImpl implements CommandExecutor {
@Override
public void runCommand(String cmd) throws IOException {
//执行操作系统命令
Runtime.getRuntime().exec(cmd);
System.out.println("'" + cmd + "' command executed.");
}
}

代理模式代理类

现在我们想让管理员用户可以执行所以命令,如果不是管理员用户,那么只允许执行部分命令。下面一个非常简单的代理类实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class CommandExecutorProxy implements CommandExecutor {

private boolean isAdmin;

private CommandExecutor executor;

public CommandExecutorProxy(String user){
if("admin".equals(user) || "root".equals(user)) isAdmin=true;
executor = new CommandExecutorImpl();
}

@Override
public void runCommand(String cmd) throws Exception {
if(isAdmin){
executor.runCommand(cmd);
}else{
if(cmd.trim().startsWith("rm")){
throw new Exception("rm command is not allowed for non-admin users.");
}else{
executor.runCommand(cmd);
}
}
}
}

代理模式客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码
public class ProxyPatternTest {

public static void main(String[] args){
CommandExecutor executor = new CommandExecutorProxy("xiaohei");
try {
executor.runCommand("ls -ltr");
executor.runCommand(" rm -rf /*");
} catch (Exception e) {
System.out.println("Exception Message::"+e.getMessage());
}
}
}

以上代理设计模式示例程序输出为:

1
2
shell复制代码'ls -ltr' command executed.
Exception Message::rm command is not allowed for non-admin users.

这样就可以完成对用户权限的控制。

代理设计模式的常见用途是控制访问或提供包装器实现以获得更好的性能。

在Java的RMI包中也使用代理模式。

代理模式类图

代理模式和适配器模式的区别

你可能会发现代理模式和适配器模式很相似,它俩有什么区别呢?

适配器模式的目的是为了改变被适配对象,而代理模式不会对被代理对象进行改变。

代理模式和装饰器模式的区别

与装饰器模式比较,装饰器模式的目的是对被代理对象的功能进行增强,代理模式是对被代理对象的访问进行控制。

小结

以上时关于代理模式的内容,理解代理模式的关键点,代理模式的目的是为了对被代理对象的访问进行控制。

我是小黑,如果对你理解代理模式有帮助,点个赞是对我最大的肯定和鼓励!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用 ArrayList 应当避免的坑

发表于 2021-11-12

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」。

大家都知道 ArrayList 是由数组实现,而数据的长度有限;需要在合适的时机对数组扩容。

当我们初始化一个长度为 2 的 ArrayList ,并往里边写入三条数据时 ArrayList 就得扩容了,也就是将之前的数据复制一份到新的数组长度为 3 的数组中。

以下是扩容的源码,之所以是 3 ,是因为新的长度=原有长度 * 1.5。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 新容量为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量发现比需要的容量还小,则以需要的容量为准
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量已经超过最大容量了,则使用最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 以新容量拷贝出来一个新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}

通过源码我们可以得知 ArrayList 的默认长度为 10。

1
2
3
4
java复制代码/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 10;

但其实并不是在初始化的时候就创建了 DEFAULT_CAPACITY = 10 的数组。而是在往里边 add 第一个数据的时候会扩容到 10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果是空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就初始化为默认大小10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
modCount++;

if (minCapacity - elementData.length > 0)
// 扩容
grow(minCapacity);
}

既然知道了默认的长度为 10 ,那说明后续一旦写入到第九个元素的时候就会扩容为 10 * 1.5 = 15。这一步为数组复制,也就是要重新开辟一块新的内存空间存放这 15 个数组。一旦我们频繁且数量巨大的进行写入时就会导致许多的数组复制,这个效率是极低的。

但如果我们提前预知了可能会写入多少条数据时就可以提前避免这个问题。

比如我们往里边写入 1000W 条数据,在初始化的时候就给定数组长度与用默认 10 的长度之间性能是差距巨大的。

用 JMH 基准测试验证一下。

提到JMH,估计有的伙伴还不知道,下面先来解释一下JMH吧。

JMH 是 Java Microbenchmark Harness 的缩写。中文意思大致是 “JAVA 微基准测试套件”。首先先明白什么是“基准测试”。

百度百科给的定义如下:

基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。

可以简单的类比成我们电脑常用的鲁大师,或者手机常用的跑分软件安兔兔之类的性能检测软件。都是按一定的基准或者在特定条件下去测试某一对象的的性能,比如显卡、IO、CPU之类的。

为什么要使用 JMH

基准测试的特质有如下几种:

可重复性:可进行重复性的测试,这样做有利于比较每次的测试结果,得到性能结果的长期变化趋势,为系统调优和上线前的容量规划做参考。

可观测性:通过全方位的监控(包括测试开始到结束,执行机、服务器、数据库),及时了解和分析测试过程发生了什么。

可展示性:相关人员可以直观明了的了解测试结果(web界面、仪表盘、折线图树状图等形式)。

真实性:测试的结果反映了客户体验到的真实的情况(真实准确的业务场景+与生产一致的配置+合理正确的测试方法)。

可执行性:相关人员可以快速的进行测试验证修改调优(可定位可分析)。

可见要做一次符合特质的基准测试,是很繁琐也很困难的。外界因素很容易影响到最终的测试结果。特别对于 JAVA的基准测试。

你运行的次数与时间不同可能获得的结果也不同,很难获得一个比较稳定的结果。

对于这种情况,有一个解决办法就是大量的重复调用,并且在真正测试前还要进行一定的预热,使结果尽可能的准确。

除了这些,对于结果我们还需要一个很好的展示,可以让我们通过这些展示结果判断性能的好坏。

而这些JMH都有!😊

如何使用 JMH

JMH是 JDK9自带的,如果你是 JDK9 之前的版本也可以通过导入 openjdk

1
2
3
4
5
6
7
8
9
10
java复制代码<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.19</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.19</version>
</dependency>

下面就用实例来演示一下:ArrayList 在初始化的时候就给定数组长度与用默认 10 的长度之间性能是差距巨大的。

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
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class CollectionsTest
{
private static final int TEN_MILLION = 10000000;

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void arrayList()
{
List<String> array = new ArrayList<>();
for (int i = 0; i < TEN_MILLION; i++)
{
array.add("123");
}
}


@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void arrayListSize()
{
List<String> array = new ArrayList<>(TEN_MILLION);
for (int i = 0; i < TEN_MILLION; i++)
{
array.add("123");
}
}


public static void main(String[] args) throws RunnerException
{
Options opt = new OptionsBuilder()
.include(CollectionsTest.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}

运行结果:

1
2
3
4
5
java复制代码# Run complete. Total time: 00:00:23

Benchmark Mode Cnt Score Error Units
CollectionsTest.arrayList avgt 5 50264.850 ± 6723.299 us/op
CollectionsTest.arrayListSize avgt 5 38389.625 ± 14797.446 us/op

根据结果可以看出预设长度的效率会比用默认的效率高上很多(这里的 Score 指执行完函数所消耗的时间)。

所以这里强烈建议大家:在有大量数据写入 ArrayList 时,一定要初始化指定长度。

一定要慎用 add(int index, E element) 向指定位置写入数据,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public void add(int index, E element) {
// 检查是否越界
rangeCheckForAdd(index);
// 检查是否需要扩容
ensureCapacityInternal(size + 1);
// 将inex及其之后的元素往后挪一位,则index位置处就空出来了
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 将元素插入到index的位置
elementData[index] = element;
// 大小增1
size++;
}

通过源码我们可以看出,每一次写入都会将 index 后的数据往后移动一遍,其实本质也是要复制数组;但区别于往常规的往数组尾部写入数据,它每次都会进行数组复制,效率极低。

小结

高性能应用都是从小细节一点点堆砌起来的,就如这里提到的 ArrayList 的坑一样,日常使用没啥大问题,一旦数据量起来所有的小问题都会成为大问题。

  • 使用 ArrayList 时如果能提前预测到数据量大小,比较大时一定要指定其长度。
  • 尽可能避免使用 add(index,e) api,会导致复制数组,降低效率。
  • 再额外提一点,我们常用的另一个 Map 容器 HashMap 也是推荐要初始化长度从而避免扩容。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Kubernetes资源管理

发表于 2021-11-12

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战。

Kubernetes资源管理

我们可以通过requests和limits给Pod指定资源配置,但如果每个Pod都要指定的话略显繁琐,我们可以使用LimitRange指定一个全局的默认配置;此外通过ResourceQuota对象,我们可以定义资源配额,这个资源配额可以为每个命名空间都提供一个总体的资源使用的限制:它可以限制命名空间中某种类型的对象的总数目上限,也可以设置命名空间中Pod可以使用的计算资源的总上限。

LimitRange

在使用LimitRange之前先创建一个命名空间:

1
2
3
4
yml复制代码apiVersion: v1
kind: Namespace
metadata:
name: test-resource

然后定义LimitRange配置(limit-range.yml):

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
30
31
32
yml复制代码apiVersion: v1
kind: LimitRange
metadata:
name: limit-range-example
spec:
limits:
- max:
cpu: 2
memory: 2Gi
min:
cpu: 200m
memory: 6Mi
maxLimitRequestRatio:
cpu: 3
memory: 2
type: Pod
- default:
cpu: 300m
memory: 200Mi
defaultRequest:
cpu: 200m
memory: 100Mi
max:
cpu: 2
memory: 1Gi
min:
cpu: 100m
memory: 3Mi
maxLimitRequestRatio:
cpu: 5
memory: 4
type: Container

创建该LimitRange:

QQ截图20191112092850.png

可以看到配置已经生效了。下面介绍下这些配置的含义:

上面配置分为Pod和Container配置,Container资源配置对应每个Docker容器的资源配置,Pod资源配置对应一个Pod中所有容器资源的总和。其中Pod和Container都可以指定min,max和maxLimitRequestRatio:

  1. min:指定资源的下限,即最低资源配置不能低于这个值;
  2. max:指定资源的上限,即最高资源使用不能高于这个值;
  3. maxLimitRequestRatio:该值用于指定requests和limits值比例的上限。

相较于Pod,Container还可以指定defaultRequest和default:

  1. defaultRequest:全局容器的默认requests值;
  2. default:全局容器的默认limits值。

下面我们举几个例子,看看我们创建的LimitRange是否生效。
定义一个Pod配置文件(test-default.yml):

1
2
3
4
5
6
7
8
9
10
yml复制代码apiVersion: v1
kind: Pod
metadata:
name: test-default
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80

创建该Pod:

QQ截图20191112094923.png

QQ截图20191112094959.png

可以看到我们在test-default.yml中并没有定义requests和limits配置,但是通过Pod实例的yaml可以看到它已经指定了这两个值,而这些正是上面我们在LimitRange中定义的默认值。

接着定义一个新的Pod配置(test-max.yml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yml复制代码apiVersion: v1
kind: Pod
metadata:
name: test-max
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
resources:
limits:
cpu: 3
memory: 500Mi

创建该Pod:

QQ截图20191112095603.png

可以看到在创建的时候就报错了,因为上面的cpu配置即超过了LimtRange中定义的Container的cpu最高配置,也超过了Pod的cpu的最高配置。

再创建一个Pod配置(test-ratio.yml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yml复制代码apiVersion: v1
kind: Pod
metadata:
name: test-ratio
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
resources:
limits:
cpu: 2
memory: 500Mi
requests:
cpu: 300m
memory: 50Mi

创建该Pod:

QQ截图20191112100308.png

可以看到也报错了,因为requests和limits的比例不符合LimitRange的maxLimitRequestRatio配置。

LimitRange的测试先到这里吧,通过上面三个例子大体也能感受到LimitRange的作用了。

ResourceQuota

ResourceQuota用于管理资源配额,这个资源配额可以为每个命名空间都提供一个总体的资源使用的限制。资源配额主要分为以下几个类型:

计算资源配额

资源名称 说明
Cpu 所有非终止状态的Pod,CPU Requests的总和不能超过该值
limits.cpu 所有非终止状态的Pod, CPU Limits的总和不能超过该值
limits.memory 所有非终止状态的Pod,内存 Limits的总和不能超过该值
Memory 所有非终止状态的Pod, 内存 Requests的总和不能超过该值
requests.cpu 所有非终止状态的Pod,CPU Requests的总和不能超过该值
requests.memory 所有非终止状态的Pod, 内存Requests的总和不能超过该值

存储资源配额

资源名称 说明
requests.storage 所有PVC,存储请求总量不能超过此值
PersistentVolumeclaims 在该命名空间中能存在的持久卷的总数上限
.storageclass.storage.k8s.io/requests.storage 和该存储类关联的所有PVC,存储请求总和不能超过此值
.storageclass.storage.k8s.io/persistentvolumeclaims 和该存储类关联的所有PVC的总和

对象数量配额

资源名称 说明
Configmaps 在该命名空间中能存在的ConfigMap的总数上限
Pods 在该命名空间中能存在的非终止状态Pod的总数上限,Pod终止状态等价于Pod的status.phase in(Failed, Succeeded) = true
Replicationcontrollers 在该命名空间中能存在的RC的总数上限
Resourcequtas 在该命名空间中能存在的资源配额项的总数上限
Services 在该命名空间中能存在的Service的总数上限
service.loadbalancers 在该命名空间中能存在的负载均衡的总数上限
services.nodeports 在该命名空间中能存在的NodePort的总数上限
Secrets 在该命名空间中能存在的Secret的总数上限

测试

在测试ResourceQuota之前我们也先创建一个命名空间:

1
2
3
4
yml复制代码apiVersion: v1
kind: Namespace
metadata:
name: quota-ns

创建个简单的ResourceQuota配置(test-quota.yml):

1
2
3
4
5
6
7
yml复制代码apiVersion: v1
kind: ResourceQuota
metadata:
name: test-quota
spec:
hard:
pods: "4"

创建该ResourceQuota:

QQ截图20191112104240.png

接着定义一个RC配置(nginx-rc.yml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yml复制代码apiVersion: v1
kind: ReplicationController
metadata:
name: nginx-rc
spec:
replicas: 3
selector:
name: nginx
template:
metadata:
labels:
name: nginx
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80

创建该RC:

QQ截图20191112104507.png

可以看到,已经有3个Pod实例了,如果将Pod数量扩大到5,看看会怎样:

QQ截图20191112104655.png

最终也只会有4个Pod实例,因为我们在上面ResourceQuota中指定的最大Pod数量为4。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

GraphQL实践1——集成JPA与MySQL

发表于 2021-11-12

主页地址

介绍

官方定义:

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。

中文官网:graphql.cn/

集成方案选择

GraphQL经过近几年的发展,SpringBoot官方、第三方都提供了starter

SpringBoot官方提供的starter:spring.io/projects/sp…

第三方提供的starter:github.com/graphql-jav…

两者活跃度都还不错,但考虑到SpringBoot官方的依赖库还未GA,而且使用起来与Spring家族耦合过大,最终决定采用第三方的starter

集成过程

数据库配置

此处采用MySQL数据库,数据内容采用MySQL官方样例sakila数据,表结构和数据地址如下
downloads.mysql.com/docs/sakila…

导入后如下

image-20211105170603665

工程配置

引入最新的SpringBoot框架,依赖如下

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
30
31
32
33
34
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!-- https://mvnrepository.com/artifact/com.graphql-java-kickstart/graphql-spring-boot-starter -->
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>12.0.0</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

增加数据库配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
yaml复制代码spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: "****"
url: jdbc:mysql://localhost:3306/sakila?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
jpa:
hibernate:
ddl-auto: update
show-sql: true
database-platform: org.hibernate.dialect.MySQL55Dialect
properties:
hibernate:
format_sql: true
use_sql_comments: true
server:
port: 8080
graphql:
tools:
schema-location-pattern: graphql/*.graphqls
graphiql:
# 开启可交互页面,用于调试
enabled: true

实体映射

新增实体映射,此处只映射Actor表作为样例

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
30
31
java复制代码package top.fjy8018.graphsqldemo.entity;

import lombok.Data;

import javax.persistence.*;
import java.sql.Timestamp;
import java.util.Objects;

/**
* 演员表实体映射
*
* @author F嘉阳
* @date 2021/11/5 10:34
*/
@Data
@Entity
@Table(name = "actor", schema = "sakila")
public class ActorEntity {
@Id
@Column(name = "actor_id", nullable = false)
private Integer actorId;

@Column(name = "first_name", nullable = false, length = 45)
private String firstName;

@Column(name = "last_name", nullable = false, length = 45)
private String lastName;

@Column(name = "last_update", nullable = false)
private Timestamp lastUpdate;
}

新增DAO

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码package top.fjy8018.graphsqldemo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import top.fjy8018.graphsqldemo.entity.ActorEntity;

/**
* 演员表DAO
*
* @author F嘉阳
* @date 2021/11/5 10:35
*/
public interface ActorEntityRepository extends JpaRepository<ActorEntity, Integer> {
}

新增查询接口配置

在resources/graphql下新增文件actor.graphqls

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码type Query {
actorList: [ActorEntity]
findOneActor(id : ID!): ActorEntity
}

type ActorEntity {
actorId: ID!
firstName: String!
lastName: String!
lastUpdate: String
}

新增查询解析器

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
30
java复制代码package top.fjy8018.graphsqldemo.resolver;

import graphql.kickstart.tools.GraphQLQueryResolver;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import top.fjy8018.graphsqldemo.entity.ActorEntity;
import top.fjy8018.graphsqldemo.repository.ActorEntityRepository;

import java.util.Collection;

/**
* 演员表GraphQL查询解析器
*
* @author F嘉阳
* @date 2021/11/5 16:27
*/
@Component
@AllArgsConstructor
public class ActorGraphQLQueryResolver implements GraphQLQueryResolver {

private final ActorEntityRepository actorRepository;

public Collection<ActorEntity> actorList() {
return actorRepository.findAll();
}

public ActorEntity findOneActor(Integer id) {
return actorRepository.findById(id).orElse(null);
}
}

启动验证

启动工程后,浏览器打开http://localhost:8080/graphiql

image-20211105172013228

列表查询

image-20211105172501790

由于采用交互式页面,输入也会有语法提示,比较方便

image-20211105172405147

唯一查询

image-20211105172219520

优缺点分析

优点

  1. 接口定义清晰,配置即接口
  2. 前端按需获取字段,减少数据传输量
  3. 接口整合、演进更加便利

缺点

  1. 减少字段个数对后端实现没有变化,并不能提升数据库性能

样例源码地址

github.com/FJiayang/gr…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python 中的 Elias Delta 编码

发表于 2021-11-12

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」

🌊 作者主页:海拥

🌊 作者简介:🥇HDZ核心组成员、🏆全栈领域优质创作者、🥈蝉联C站周榜前十

🌊 粉丝福利:进粉丝群每周送四本书(每位都有),每月抽送各种小礼品(掘金搪瓷杯、抱枕、鼠标垫、马克杯等)

在本文中,我们将使用 python 实现 Elias Delta 编码。

语法:

1
Python复制代码Elias Delta Encoding(X)= Elias Gamma encoding (1+floor(log2(X)) + Binary representation of X without MSB.

分步实施

首先,在为 Elias Delta 编码编写代码之前,我们将实现 Elias delta 编码。

第1步:

  • 从数学库导入 log、floor 函数以执行对数运算。
  • 从用户获取输入 k 以在 Elias Gamma 中进行编码。
  • 使用数学模块中的 floor 和 log 函数,找到 1+floor(log2(X) 并将其存储在变量 N 中。
  • 使用 (N-1)*‘0’+’1’ 找到 N 的一元编码,它为我们提供了一个二进制字符串,其中最低有效位为 ‘1’,其余最高有效位为 N-1 个’0’。

示例: 某些值的 Elias Gamma 编码

1
2
3
4
5
6
Python复制代码def EliasGammaEncode(k):
if (k == 0):
return '0'
N = 1 + floor(log(k, 2))
Unary = (N-1)*'0'+'1'
return Unary + Binary_Representation_Without_MSB(k)

第2步:

  • 创建一个函数,该函数接受输入 X 并给出结果作为 X 的二进制表示,没有 MSB。
  • 使用“{0:b}”.format(k) 找到 k 的二进制等效项并将其存储在名为 binary 的变量中。
+ 前缀零仅指定应使用 format() 的哪个参数来填充 {}。
+ b 指定参数应转换为二进制形式。
  • 返回字符串 binary[1:],它是 X 的二进制表示,没有 MSB。

示例: 不带 MSB 的二进制表示

1
2
3
4
Python复制代码def Binary_Representation_Without_MSB(x):
binary = "{0:b}".format(int(x))
binary_without_MSB = binary[1:]
return binary_without_MSB

现在我们要为 Elias Delta Encoding 编写代码

第 3 步:

  • 从用户获取输入 k 以在 Elias Delta 中进行编码。
  • 使用数学模块中的 floor 和 log 函数,找到 1+floor(log2(k)。
  • 将 1+floor(log2(k) 的结果传递给 Elias Gamma 编码函数。

示例:某些值的 Elias Delta 编码

1
2
3
4
5
6
7
8
Python复制代码def EliasDeltaEncode(x):
Gamma = EliasGammaEncode(1 + floor(log(k, 2)))
binary_without_MSB = Binary_Representation_Without_MSB(k)
return Gamma+binary_without_MSB


k = int(input('Enter a number to encode in Elias Delta: '))
print(EliasDeltaEncode(k))

第四步:

  • 得到不带 MSB 的 k 的 Elias Gamma 编码和二进制表示的结果
  • 连接两个结果并在控制台上打印它们

为某些整数值生成 Elias Delta 编码的完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Python复制代码from math import log
from math import floor

def Binary_Representation_Without_MSB(x):
binary = "{0:b}".format(int(x))
binary_without_MSB = binary[1:]
return binary_without_MSB

def EliasGammaEncode(k):
if (k == 0):
return '0'
N = 1 + floor(log(k, 2))
Unary = (N-1)*'0'+'1'
return Unary + Binary_Representation_Without_MSB(k)

def EliasDeltaEncode(x):
Gamma = EliasGammaEncode(1 + floor(log(k, 2)))
binary_without_MSB = Binary_Representation_Without_MSB(k)
return Gamma+binary_without_MSB

k = 14
print(EliasDeltaEncode(k))

输出:

1
Python复制代码00100110

写在最后的

作者立志打造一个拥有100个小游戏的摸鱼网站,更新进度:40/100

我已经写了很长一段时间的技术博客,并且主要通过掘金发表,这是我的一篇关于Python 中的 Elias Delta 编码。我喜欢通过文章分享技术与快乐。你可以访问我的博客: juejin.cn/user/204034… 以了解更多信息。希望你们会喜欢!😊

💌 欢迎大家在评论区提出意见和建议!💌

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…364365366…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%