精华推荐 | 【深入浅出RocketMQ原理及实战】「性能原理挖掘系列」透彻剖析贯穿RocketMQ的系统服务底层原理以及高性能存储设计挖掘深入

news/2024/5/22 1:20:11/文章来源:https://blog.csdn.net/l569590478/article/details/128032717

设计背景

消息中间件的本身定义来考虑,应该尽量减少对于外部第三方中间件的依赖。一般来说依赖的外部系统越多,也会使得本身的设计越复杂,采用文件系统作为消息存储的方式。

RocketMQ存储机制

消息中间件的存储一般都是利用磁盘,一般是使用机械硬盘,但机械硬盘的速度比访问内存慢了n个数量级,一款优秀的消息中间件必然会将硬件资源压榨到极致,接下来看看rocketMq是如何做到高效存储的。

RocketMQ存储模型

CommitLog

消息主体以及元数据的存储媒介,存储Producer端写入的消息主体内容。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如,00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件;

ConsumeQueue

  • 消息消费的逻辑队列,其中包含了这个MessageQueue在CommitLog中的起始物理位置偏移量offset,消息实体内容的大小和Message Tag的哈希值。

  • 实际物理存储来说,ConsumeQueue对应每个Topic和QueueId下面的文件,单个文件大小约5.72M,每个文件由30W条数据组成,每个文件默认大小为600万个字节,当一个ConsumeQueue类型的文件写满了,则写入下一个文件;

IndexFile

生成的索引文件提供访问服务,通过消息Key值查询消息真正的实体内容。在实际的物理存储上,文件名则是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存2000W个索引;

MapedFileQueue

对连续物理存储的抽象封装类,可以通过消息存储的物理偏移量位置快速定位该offset所在MappedFile(具体物理存储位置的抽象)、创建、删除MappedFile等操作;

MappedFile

文件存储的直接内存映射业务抽象封装类,通过操作该类,可以把消息字节写入PageCache缓存区(commit),或者原子性地将消息持久化的刷盘(flush);

RocketMQ消息架构

集群有一个Broker,Topic为binlog的队列(Consume Queue)数量为4,如下图所示,按顺序发送消息。Consumer端线程采用的是负载订阅的方式进行消费。

Commit Log和Consume Queue

消息发送流程架构

发送到相关服务节点

生产到消费的转换

总体核心流程

RocketMQ的消息整体是有序的,所以消息按顺序将内容持久化在Commit Log中。Consume Queue则是用于将消息均衡地按序排列在不同的逻辑队列,集群模式下多个消费者就可以并行消费Consume Queue的消息。

  1. MappedFile 所有的topic数据都写到同一个文件中,文件的大小默认为1G,使用mmap与磁盘文件做映射,初始化时使用mlock将内存锁定,防止pagecache被os交换到swap区域。数据是顺序写,数据写满后自动创建下个MappedFile顺序写入。
  2. MappedFileQueue MappedFile的队列,存储封装了所有的MappedFile实例。
  3. CommitLog 封装了写入消息和读取消息的实现,根据MappedFileQueue找到正在写的MappedFile,之后将消息写入到pagecache,最后同步到硬盘上。
  4. ConsumerQueue 一个topic可以设置多个queue,每个consumerQueue对应一个topic下的queue,相当于kafka里的partition概念。里面存储了msg在commitLog中的offset、size、tagsCode,固定长度是20字节,consumer可以根据消息的offset在commitLog找到具体的消息。

详细分析MQ发送和消费流程

消息生产和消费通过CommitLog

生产者发送消息最终写入的是CommitLog(消息存储的日志数据文件),Consumer端先从ConsumeQueue(消息逻辑队列)读取持久化消息的起始物理位置偏移量offset、大小size和消息Tag的HashCode值,随后再从CommitLog中进行读取待拉取消费消息的真正实体内容部分;

IndexFile(索引文件)

为了消息查询提供了一种通过key或时间区间来查询消息的方法, 通过IndexFile来查找消息的方法不影响发送与消费消息的主流程。

RocketMQ的CommitLog文件采用混合型存储

所有Topic下的消息队列共用同一个CommitLog的日志数据文件,并通过建立类似索引文件—ConsumeQueue的方式来区分不同Topic下面的不同MessageQueue的消息,同时为消费消息起到一定的缓冲作用。

  • 只有ReputMessageService异步服务线程通过doDispatch异步生成了ConsumeQueue队列的元素后,Consumer端才能进行消费。

  • 只要消息写入并刷盘至CommitLog文件后,消息就不会丢失,即使ConsumeQueue中的数据丢失,也可以通过CommitLog来恢复。

RocketMQ顺序读写

  • 发送消息时,生产者端的消息确实是顺序写入CommitLog;

  • 消费消息时,消费者端也是顺序读取ConsumeQueue,根据其中的起始物理位置偏移量offset读取消息是随机读取CommitLog。

在RocketMQ集群整体的吞吐量、并发量非常高的情况下,随机读取文件带来的性能开销影响还是比较大的

RocketMQ存储架构的优缺点:

优点:
  1. ConsumeQueue消息逻辑队列较为轻量级;
  2. 磁盘的访问串行化,避免磁盘竟争,不会因为队列增加导致IOWAIT增高;
缺点:
  1. CommitLog来说写入消息虽然是顺序写,但是读却变成了完全的随机读;
  2. Consumer端订阅消费一条消息,需要先读ConsumeQueue,再读Commit Log,一定程度上增加了开销;

RocketMQ存储模型

RocketMQ文件存储模型,根据类别和作用从概念模型上大致可以划分为5层

  1. RocketMQ业务处理器层: Broker端对消息进行读取和写入的业务逻辑入口,这一层主要包含了业务逻辑相关处理操作(根据解析RemotingCommand中的RequestCode来区分具体的业务操作类型,进而执行不同的业务处理流程),比如前置的检查和校验步骤、构造MessageExtBrokerInner对象、decode反序列化、构造Response返回对象等;

  2. RocketMQ数据存储组件层:该层主要是RocketMQ的存储核心类—DefaultMessageStore,其为RocketMQ消息数据文件的访问入口,通过该类的“putMessage()”和“getMessage()”方法完成对CommitLog消息存储的日志数据文件进行读写操作(具体的读写访问操作还是依赖下一层中CommitLog对象模型提供的方法);在该组件初始化时候,还会启动很多存储相关的后台服务线程:

    • AllocateMappedFileService(MappedFile预分配服务线程)
    • ReputMessageService(回放存储消息服务线程)
    • HAService(Broker主从同步高可用服务线程)
    • StoreStatsService(消息存储统计服务线程)
    • IndexService(索引文件服务线程)等;
  3. RocketMQ存储逻辑对象层: 该层主要包含了RocketMQ数据文件存储直接相关的三个模型类IndexFile、ConsumerQueue和CommitLog。

    • IndexFile为索引数据文件提供访问服务
    • ConsumerQueue为逻辑消息队列提供访问服务
    • CommitLog则为消息存储的日志数据文件提供访问服务。

这三个模型类也是构成了RocketMQ存储层的整体结构;

  1. 封装的文件内存映射层: RocketMQ主要采用JDK NIO中的MappedByteBuffer和FileChannel两种方式完成数据文件的读写。

    • 采用MappedByteBuffer这种内存映射磁盘文件的方式完成对大文件的读写,在RocketMQ中将该类封装成MappedFile类。
    • 对于每类大文件(IndexFile/ConsumerQueue/CommitLog),在存储时分隔成多个固定大小的文件(单个IndexFile文件大小约为400M、单个ConsumerQueue文件大小约5.72M、单个CommitLog文件大小为1G),其中每个分隔文件的文件名为前面所有文件的字节大小数+1,即为文件的起始偏移量,从而实现了整个大文件的串联。

每一种类的单个文件均由MappedFile类提供读写操作服务(其中,MappedFile类提供了顺序写/随机读、内存数据刷盘、内存清理等和文件相关的服务);

  1. 磁盘存储层: 主要指的是部署RocketMQ服务器所用的磁盘。

RocketMQ存储技术

主要采用mmap与PageCache,其中mmap内存映射技术—Java中的MappedByteBuffer。

先简单介绍下mmap

mmap一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上。内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

mmap内存映射和普通标准IO操作的本质区别在于它并不需要将文件中的数据先拷贝至OS的内核IO缓冲区,而是可以直接将用户进程私有地址空间中的一块区域与文件对象建立映射关系,这样程序就好像可以直接从内存中完成对文件读/写操作一样。

只有当缺页中断发生时,直接将文件从磁盘拷贝至用户态的进程空间内,只进行了一次数据拷贝。对于容量较大的文件来说(文件大小一般需要限制在1.5~2G以下,这也是CommitLog设置成1G的原因),采用Mmap的方式其读/写的效率和性能都非常高。

  • RocketMq默认的文件大小为1G,即将1G的文件映射到物理内存上。但mmap初始化时只是将文件磁盘地址和进程虚拟地址做了个映射,并没有真正的将整个文件都映射到内存中,当程序真正访问这片内存时产生缺页异常,这时候才会将文件的内容拷贝到page cache。

如果一开始只是做个映射,而到具体写消息时才将文件的部分页加载到pagecache,那效率将会是多么的低下。MappedFile初始化的操作是由单独的线程(AllocateMappedFileService)实现的,就是对应的生产消费模型。RocketMq在初始化MappedFile时做了内存预热,事先向page cache 中写入一些数据flush到磁盘,使整个文件都加载到page cache中。

MappedByteBuffer技术分析

MappedByteBuffer继承自ByteBuffer,其内部维护了一个逻辑地址变量—address。在建立映射关系时,

MappedByteBuffer利用了JDK NIO的FileChannel类提供的map()方法把文件对象映射到虚拟内存。

源码中map()方法的实现,可以发现最终其通过调用native方法map0()完成文件对象的映射工作,同时使用Util.newMappedByteBuffer()方法初始化MappedByteBuffer实例,但最终返回的是DirectByteBuffer的实例。

在Java程序中使用MappedByteBuffer的get()方法来获取内存数据是最终通过DirectByteBuffer.get()方法实现(底层通过unsafe.getByte()方法,以“地址 + 偏移量”的方式获取指定映射至内存中的数据)。

使用Mmap的限制
  • mmap映射的内存空间释放的问题

由于映射的内存空间本身就不属于JVM的堆内存区(Java Heap),因此其不受JVM GC的控制,卸载这部分内存空间需要通过系统调用unmap()方法来实现。

然而unmap()方法是FileChannelImpl类里实现的私有方法,无法直接显示调用。RocketMQ中的做法是,通过Java反射的方式调用“sun.misc”包下的Cleaner类的clean()方法来释放映射占用的内存空间;

  • MappedByteBuffer内存映射大小限制

因为其占用的是虚拟内存(非JVM的堆内存),大小不受JVM的-Xmx参数限制,但其大小也受到OS虚拟内存大小的限制。一般来说,一次只能映射1.5~2G 的文件至用户态的虚拟内存空间,RocketMQ默认设置单个CommitLog日志数据文件为1G的原因了;

  • 使用MappedByteBuffe的其他问题

会存在内存占用率较高和文件关闭不确定性的问题;

OS的PageCache机制

PageCache是OS对文件的缓存,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写访问,这里的主要原因就是在于OS使用PageCache机制对读写访问操作进行了性能优化,将一部分的内存用作PageCache。

对于数据文件的读取

如果一次读取文件时出现未命中PageCache的情况,OS从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取。这样,只要下次访问的文件已经被加载至PageCache时,读取操作的速度基本等于访问内存。

对于数据文件的写入

OS会先写入至Cache内,随后通过异步的方式由pdflush内核线程将Cache内的数据刷盘至物理磁盘上。

对于文件的顺序读写操作来说,读和写的区域都在OS的PageCache内,此时读写性能接近于内存。

  • RocketMQ的大致做法是,将数据文件映射到OS的虚拟内存中(通过JDK NIO的MappedByteBuffer),写消息的时候首先写入PageCache,并通过异步刷盘的方式将消息批量的做持久化(同时也支持同步刷盘);

  • 订阅消费消息时(对CommitLog操作是随机读取),由于PageCache的局部性热点原理且整体情况下还是从旧到新的有序读,因此大部分情况下消息还是可以直接从Page Cache中读取,不会产生太多的缺页(Page Fault)中断而从磁盘读取。

PageCache机制也不是完全无缺点的,当遇到OS进行脏页回写,内存回收,内存swap等情况时,就会引起较大的消息读写延迟。

对于这些情况,RocketMQ采用了多种优化技术,比如内存预分配,文件预热,mlock系统调用等,来保证在最大可能地发挥PageCache机制优点的同时,尽可能地减少其缺点带来的消息读写延迟。

RocketMQ存储优化技术

RocketMQ存储层采用的几项优化技术方案在一定程度上可以减少PageCache的缺点带来的影响,主要包括内存预分配,文件预热和mlock系统调用。

预先分配MappedFile

在消息写入过程中(调用CommitLog的putMessage()方法),CommitLog会先从MappedFileQueue队列中获取一个 MappedFile,如果没有就新建一个。

MappedFile的创建过程是将构建好的一个AllocateRequest请求(具体做法是,将下一个文件的路径、下下个文件的路径、文件大小为参数封装为AllocateRequest对象)添加至队列中,后台运行的AllocateMappedFileService服务线程(在Broker启动时,该线程就会创建并运行),会不停地run,只要请求队列里存在请求,就会去执行MappedFile映射文件的创建和预分配工作。

分配的时候有两种策略,

一种是使用Mmap的方式来构建MappedFile实例,另外一种是从TransientStorePool堆外内存池中获取相应的DirectByteBuffer来构建MappedFile。并且,在创建分配完下个MappedFile后,还会将下下个MappedFile预先创建并保存至请求队列中等待下次获取时直接返回。RocketMQ中预分配MappedFile的设计非常巧妙,下次获取时候直接返回就可以不用等待MappedFile创建分配所产生的时间延迟。

文件预热

预热的目的主要有两点;

  • 第一点,由于仅分配内存并进行mlock系统调用后并不会为程序完全锁定这些内存,因为其中的分页可能是写时复制的。因此,就有必要对每个内存页面中写入一个假的值。其中,RocketMQ是在创建并分配MappedFile的过程中,预先写入一些随机值至Mmap映射出的内存空间里。

  • 第二,调用Mmap进行内存映射后,OS只是建立虚拟内存地址至物理地址的映射表,而实际并没有加载任何文件至内存中。程序要访问数据时OS会检查该部分的分页是否已经在内存中,如果不在,则发出一次缺页中断。这里,可以想象下1G的CommitLog需要发生多少次缺页中断,才能使得对应的数据才能完全加载至物理内存中。

RocketMQ的做法是,在做Mmap内存映射的同时进行madvise系统调用,目的是使OS做一次内存映射后对应的文件数据尽可能多的预加载至内存中,从而达到内存预热的效果。

public void warmMappedFile(FlushDiskType type, int pages) {long beginTime = System.currentTimeMillis();// mappedByteBuffer在java里面对应了mmap的实现ByteBuffer byteBuffer = this.mappedByteBuffer.slice();int flush = 0;long time = System.currentTimeMillis();for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {byteBuffer.put(i, (byte) 0);// force flush when flush disk type is syncif (type == FlushDiskType.SYNC_FLUSH) {// 同步刷盘机制,OS_PAGE_SIZE为4Kif ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {flush = i;mappedByteBuffer.force();}}// prevent gcif (j % 1000 == 0) {log.info("j={}, costTime={}", j, System.currentTimeMillis() - time);time = System.currentTimeMillis();try {Thread.sleep(0);} catch (InterruptedException e) {log.error("Interrupted", e);}}}// force flush when prepare load finishedif (type == FlushDiskType.SYNC_FLUSH) {log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}",this.getFileName(), System.currentTimeMillis() - beginTime);mappedByteBuffer.force();}log.info("mapped file warm-up done. mappedFile={}, costTime={}", this.getFileName(),System.currentTimeMillis() - beginTime);// 将page cache 这片内存锁定this.mlock();}

mlock 内存锁定

  • OS在内存充足的情况下,会将文件加载到 page cache 提高文件的读写效率,但是当内存不够用时,os会将page cache回收掉。试想如果MappedFile对应的pagecache 被os回收,那就又产生缺页异常再次从磁盘加载到pagecache,会对系统性能产生很大的影响。

  • 将进程使用的部分或者全部的地址空间锁定在物理内存中,防止其被交换到swap空间。对于RocketMQ这种的高吞吐量的分布式消息队列来说,追求的是消息读写低延迟,那么肯定希望尽可能地多使用物理内存,提高数据读写访问的操作效率。

RocketMq在创建完MappedFile并且内存预热完成后调用了c的mlock函数将这片内存锁定了,具体来看下是怎么实现的

// java 调用c
LibC INSTANCE = (LibC) Native.loadLibrary(Platform.isWindows() ? "msvcrt" : "c", LibC.class);
// 具体实现
public void mlock() {final long beginTime = System.currentTimeMillis();final long address = ((DirectBuffer) (this.mappedByteBuffer)).address();Pointer pointer = new Pointer(address);{int ret = LibC.INSTANCE.mlock(pointer, new NativeLong(this.fileSize));log.info("mlock {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime);}{int ret = LibC.INSTANCE.madvise(pointer, new NativeLong(this.fileSize), LibC.MADV_WILLNEED);log.info("madvise {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime);}
}

RocketMQ刷盘机制

写消息时是先写入到pagecache,rocketMq提供了两种刷盘机制,同步刷盘和异步刷盘,同步刷盘适用于对消息可靠性比较高的场合,同步刷盘性能比较低下,这样即使系统宕机消息也不会丢失。

同步刷盘

  • RocketMQ的Broker端才会真正地返回给Producer端一个成功的ACK响应。同步刷盘对MQ消息可靠性来说是一种不错的保障,但是性能上会有较大影响,一般适用于金融业务应用领域。

  • RocketMQ同步刷盘的大致做法是,基于生产者消费者模型,主线程创建刷盘请求实例—GroupCommitRequest并在放入刷盘写队列后唤醒同步刷盘线程—GroupCommitService,来执行刷盘动作(其中用了CAS变量和CountDownLatch来保证线程间的同步)。RocketMQ中用读写双缓存队列(requestsWrite/requestsRead)来实现读写分离,其带来的好处在于内部消费生成的同步刷盘请求可以不用加锁,提高并发度。

  • 刷盘线程从阻塞队列中获取,刷盘其实就是调用了mappedByteBuffer.force()方法,刷盘成功后通过countdownlatch唤醒刷盘等待的线程,原理很简单>


public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {// 同步刷盘if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {// 对应一个单独的线程final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;if (messageExt.isWaitStoreMsgOK()) {// GroupCommitRequest 封装了CountDownLatch,GroupCommitService刷盘完毕后唤醒等待线程GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());service.putRequest(request);boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());if (!flushOK) {log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags()+ " client address: " + messageExt.getBornHostString());putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);}} else {service.wakeup();}}// 异步刷盘else {if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {flushCommitLogService.wakeup();} else {commitLogService.wakeup();}}}

异步刷盘

  • 异步刷盘原理 发送消息线程写到pagecache成功之后就返回,消息保存在page cache 中,异步刷盘对应了一个单独线程,刷盘默认一次刷4个pageSize,也就是16k的数据。异步刷盘有可能会丢失数据,当jvm程序死掉 但机器没有宕机,pagecache中的脏页还是能人工刷到磁盘的,但是当机器宕机之后,数据就永远丢失了。

  • 能够充分利用OS的PageCache的优势,只要消息写入PageCache即可将成功的ACK返回给Producer端。消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了MQ的性能和吞吐量。异步和同步刷盘的区别在于,异步刷盘时,主线程并不会阻塞,在将刷盘线程wakeup后,就会继续执行。

RocketMQ的堆外存储机制

  • RocketMq提供了堆外内存池机制即TransientStorePool,TransientStorePool初始化时实例化5个堆外内存,大小和MappedFile的大小1G,然后mlock锁定此内存区域。

  • 发送消息时如果开启了堆外内存机制,MappedFile在实例化时从堆外内存池中获取一个directBuffer实例,写消息先写到堆外内存中,然后有单独的线程(CommitRealTimeService)刷到pagecache,之后再由单独的线程(FlushRealTimeService)从pagecahce刷到磁盘。

开启堆外内存池的好处:写消息时先写到堆外内存,纯内存操作非常快。读消息时是从pagecache中读,相当于实现了读写分离,但是会存在延时性机制问题,以及对外内存宕机了会丢失,数据一致性会存在问题。

消息生产

所有发送消息的线程是串行执行的,所有topic的数据放一块顺序写到pagecache中,因此效率十分的高。在写 page cache 成功后,再由单独的线程异步构建consumerQueue和 indexFile(基于磁盘实现的hashMap,实现消息的查找),构建完成consumerQueue成功后 consumer 就能消费到最新的消息了,当然构建consumerQueue也是顺序写,每次只写入20个字节,占用的空间也不大。

消息消费

每个topic可以对应多个consumerQueue,就相当于kafka里面的分区概念,Rocketmq里面的消费者与consumerQueue的分配算法和kafka的相似。由于consumerQueue中只保存了消息在commitLog中的offset、msgSize、tagsCode,因此需要拿到offset去commitlog中把这条消息捞出来,这时候读相当与随机读。

注意,由前面的mlock内存锁定再加上消费的数据一般是最近生产的,数据还在pagecache中,对性能的影响也不大,当consumer消费很远的数据时,pagecache中肯定是没有缓存的,这时候rocketMq建议consumer去slave上读

总结

RocketMq所有topic共用一个commitLog,磁盘顺序写,这一点实现也是参考了kafka,读消息时根据consumerQueue去commitLog中吧数据捞出来,虽然是随机读,但是最新的数据一般在pagecahce中也无关紧要。使用内存锁定避免内存swap交换,堆外内存和pagecache的读写分离。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_36542.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

餐饮+KTV融合消费模式,会受消费者喜欢吗?

这个五一&#xff0c;我们雨科网门店系统的客户&#xff0c;大侠火锅店终于是将KTV搬到了自己的门店里&#xff0c;运用门店小程序功能及纸质代金券及礼品的噱头吸引客户进店&#xff0c;只需消费并和任意一人合唱一首歌即可领取&#xff0c;消费者在等餐或放松的时候一键点歌演…

OneAuth 2022.11.23版本更新内容

2022.11.23版本更新内容&#xff1a; 新增IdP飞书 云目录增加对Group的支持GWA浏览器插件适配性优化自定义授权服务器优化&#xff0c;适应RBAC、ABAC等多种场景授权IdP 北森优化&#xff0c;适配自定义的属性租户的部分试用功能需要联系后台开通其他一些Bug的修复 标题新增 …

PDF预览完整解决方案及各种兼容(VUE版)

PDF预览完整解决方案及各种兼容&#xff08;VUE版&#xff09; PDF预览完整解决方案及各种兼容&#xff08;VUE版&#xff09; - 掘金 前端学习使者正在上传…重新上传取消 2021年11月12日 16:57 阅读 2547 一、利用iframe 就一行代码就够了&#xff0c;只能满足最基本的…

ThinkPHP5目录结构

文章目录一、TP5的框架的下载1、[采用fastAdmin安装](https://www.fastadmin.net/download.html)2、Composer安装2.1 Composer提供的服务3、Git安装二、使用Composer安装后目录结构2.1 补充获取 Git 仓库git的工作机制一、TP5的框架的下载 1、采用fastAdmin安装 FastAdmin是一…

运营版uniapp多商户商城小程序+H5+APP+商家入驻短视频社区种草直播阶梯拼团

运营版uniapp多商户商城小程序H5APP商家入驻短视频社区种草直播阶梯拼团 前后端全套源码&#xff0c; 支持二次开发&#xff0c;代码无加密&#xff01; 独立商家后台 用于店铺商品管理订单管理发货管理等 多类经营模式 多商家B2B2C、自营B2C运营模式 私有化部署 前端Uni…

JVM类加载(类加载过程、双亲委派模型)

系列文章目录 JVM的内存区域划分_crazy_xieyi的博客-CSDN博客 文章目录 一、类加载过程二、关于类加载的典型试题三、双亲委派模型一、类加载过程 对于一个类来说&#xff0c;它的生命周期是这样的&#xff1a;1.加载 “加载”&#xff08;Loading&#xff09;阶段是整个“类加…

spring 如何解决循环依赖

什么是循环依赖 A 类中有一个属性 B &#xff0c;也就是说 A 依赖 B&#xff0c;同时 B 类中有一个属性 A, 也就是说 B 依赖 A. 他们之间的依赖关系形成了环。就是我们说的循环依赖&#xff0c;如下图&#xff1a; 循环依赖示例 public class CircularDependenciesDemo {publ…

SSM基于上述环境实现简单CUDA操作

目录 1. 结构 2. 环境&#xff1a; 3. controller 4. mapper 5. service 6. serviceImpl 7. mapper.xml 8. emplist.html 9. update 1. 结构 2. 环境&#xff1a; SSM整合 Spring SprintMVC Mybatishttps://blog.csdn.net/qq_41950447/article/details/128033971 3.…

Android -- 每日一问:Activity的启动模式(launchMode)有哪些,有什么区别?

经典回答 这应该是一道很虐人的面试题&#xff0c;很多人都答不上来&#xff0c;很多人根本就没有用过。当我发现在被我面试的人中有80%的比例对它不了解时&#xff0c;我找过一些同事讨论是否还有在面试中考查这个问题的必要&#xff0c;得到的回答是“程序员何苦为难程序员”…

2020-RKT

2020-RKT&#xff1a;Relation-Aware Self-Attention for Knowledge Tracing 有代码&#xff1a;https://github.com/shalini1194/RKT 摘要 学生在解决练习的过程中获得技能&#xff0c;每一次这样的互动都对学生解决未来练习的能力有明显的影响。 这种影响表现为:1)互动中涉…

电脑c盘满了怎么清理,快速清理,用这5招

​新买的电脑没用多久&#xff0c;突然发现系统提示磁盘空间不足。点击一看&#xff0c;电脑c盘空间已经爆满变红。当出现这种情况时&#xff0c;很多电脑的运行速度会大大降低&#xff0c;甚至导致部分应用无法正常运行。那么电脑c盘满了怎么清理&#xff1f;如何释放电脑c盘空…

C语言:关键字----switch、case、default(开关语句)

C语言&#xff1a;基础开发----目录 C语言&#xff1a;关键字—32个(分类说明) 有32个关键字详细说明&#xff0c;还有跳转链接&#xff01; 一、开关语句----介绍 开关语句&#xff0c;包括以下四种关键字&#xff1a; switch&#xff1a;开关语句case&#xff1a; 开关语句…

【vim】系统剪切板、vim寄存器之间的复制粘贴操作命令?系统剪切板中的内容复制粘贴到命令行?vim文本中复制粘贴到命令行

一、系统剪切板和文本内容的复制粘贴 1.1 从系统剪切板复制粘贴到文本中 需要操作3次&#xff1a; 分别是英文双引号、一个加号或梅花号&#xff0c;最后是一个p 也即"p 或者直接使用组合键【Shift insert】 1.2 从文本复制粘贴到系统剪切板 也需要操作3次&#xff…

java EE初阶 — 计算机工作原理

文章目录1.操作系统2.操作系统的定位3.进程3.1 进程的基本了解3.2 操作系统内核是如何管理软件资源的3.3 PCB里描述了进程的哪些特征3.3.1 三个较为简单的特征3.3.2 进程的调度属性4.内存管理1.操作系统 操作系统是一个搞管理的软件。 对上要给软件提供稳定的运行环境。对下要…

基于JAVA的鲜花店商城平台【数据库设计、源码、开题报告】

数据库脚本下载地址&#xff1a; https://download.csdn.net/download/itrjxxs_com/86427660 摘要 在互联网不断发展的时代之下&#xff0c;鲜花软件可以为鲜花企业带来更多的发展机会&#xff0c;让企业可以挖掘到更多的潜在用户&#xff0c;同时结合企业的优势就能够为用户…

Swin Transformer目标检测实验——环境配置的步骤和避坑

Swin Transformer1. 网上基础教程&#xff08;带视频讲解&#xff09;2. 配置虚拟环境时遇到的一些问题&#xff08;按操作顺序排列&#xff09;1. 网上基础教程&#xff08;带视频讲解&#xff09; 大家是不是都从b站来的呀&#xff0c;先给你们基础环境的配置和搭配的视频教…

黑马点评--Redis消息队列

Redis消息队列 Redis消息队列实现异步秒杀 消息队列&#xff08;Message Queue&#xff09;&#xff0c;字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色&#xff1a; 消息队列&#xff1a;存储和管理消息&#xff0c;也被称为消息代理&#xff08;Message Br…

【附源码】计算机毕业设计JAVA疫情下的居民管理系统

【附源码】计算机毕业设计JAVA疫情下的居民管理系统 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; JAVA…

蒙泰转债上市价格预测

蒙泰转债基本信息转债名称&#xff1a;蒙泰转债&#xff0c;评级&#xff1a;A&#xff0c;发行规模&#xff1a;3.0亿元。正股名称&#xff1a;蒙泰高新&#xff0c;今日收盘价&#xff1a;31.3&#xff0c;转股价格&#xff1a;26.15。当前转股价值 转债面值 / 转股价格 * 正…

有没有把语音转为文字的软件?这几个转换软件你值得收藏

我们在日常的工作和生活中&#xff0c;应该经常会遇到需要将音频转换成文字的情况吧。相信大部分的小伙伴都会选择直接使用转换软件进行音频转文字的操作&#xff0c;但在使用的过程中就会发现&#xff0c;有些软件会在使用次数、音频时长上面有所限制&#xff0c;导致我们会转…