Linux 文件系统解析(三)cache

Linux文件系统中使用了大量cache,用于提升IO性能,本篇来梳理一下这些与文件系统相关的cache,它们在内存中是如何组织管理的,它们是如何加速文件系统操作的。

Dentry Cache

dentry用于描述系统目录树中的一个节点,磁盘文件系统中通常没有相关结构,dentry只存在于内存之中,它的主要作用是提供了一种快速的通过路径名查找到对应文件的方法。

组织方式

dentry中用于组织管理的数据结构如下:

struct dentry {
    ...
	struct hlist_bl_node d_hash;	/* lookup hash list */    //dentry_hashtable全局hash表中的节点
	struct qstr d_name;    //根据其->hash来计算该dentry在dentry_hashtable中的索引
    ...
	union {
		struct list_head d_lru;		/* LRU list */    //其d_sb->s_dentry_lru中的节点
		wait_queue_head_t *d_wait;	/* in-lookup ones only */
	};
	struct list_head d_child;	/* child of parent list */   //其d_parent->d_subdirs中的节点
	struct list_head d_subdirs;	/* our children */    //子dentry链表
    ...
	union {
		struct hlist_node d_alias;	/* inode alias list */    //在其对应inode->i_dentry中的节点(一个inode可能有多个硬连接,在inode中需要用一个链表管理这些dentry)
		struct hlist_bl_node d_in_lookup_hash;	/* only for in-lookup ones */
	 	struct rcu_head d_rcu;
	} d_u;
}

dentry结构体由struct kmem_cache *dentry_cache 分配,dentry_cache 在内核初始化流程中 dcache_init() 函数里初始化。同时该函数中还会初始化管理dentry的全局hash表dentry_hashtable。

所有dentry总是处于三种状态:

  • unused,未被内核使用,引用计数d_lockref为0,d_inode非空。
  • inuse,正在被使用,引用计数d_lockref>0。
  • negative,相应的磁盘inode已经被删除,d_inode为空。

dentry结构通常在路径查找流程中被创建,主要有以下四种管理方式:

  • dentry_hashtable:以dentry->d_name.hash作为索引在dentry全局hash表中管理,用于路径查找。
  • superblock->s_dentry_lru:管理所有unused和negative的dentry,当需要回收内存时,从中选取“最少使用的”dentry回收。
  • d_subdirs:dentry结构中记录其子dentry,用于dir管理。
  • inode->i_dentry:由于一个inode可能有多个硬链接,这些dentry称为alias,使用双向链表管理,每个inuse的dentry加入其inode->i_dentry链表。

其中用的最多的是dentry_hashtable,每次open文件时,都要反复访问它来查找对应的文件。

path-lookup流程

path-lookup(Documentation/filesystems/path-lookup.txt)是根据一个路径名,逐级查找dentry,最终找到目标文件dentry的过程。既然有了dentry cache,基本策略就是先从cache里找,如果找不到再通过磁盘数据生成dentry,直到得到最终文件的dentry,而内核设计了复杂的流程用于提升查找性能。

由于dentry在系统中可能被并发的访问及修改,为了保证查找数据的正确性并兼顾性能,内核设计了两种查找方式。

  • ref-walk,传统的cache访问方式,使用自旋锁以及引用计数来保证数据正确性,这使得查找路径中的每个dentry都会有加锁的开销以及可能发生阻塞,造成性能下降。
  • rcu-walk,基于顺序锁的查询方式,在查询过程中不用加锁,十分高效,若rcu-walk查询失败再尝试切换到ref-walk模式继续后面的查询。

这两种方式都是从dentry_hashtable中查找(lookup_fast),若dentry_hashtable中无此dentry,再通过inode->i_op->lookup查找(lookup_slow)。path-lookup整体流程如下:

  • 以rcu-walk模式逐级查找dentry,并校验正确性。
  • 若rcu-walk出现校验失败,从头开始以ref-walk模式查找。
  • 若rcu-walk出现查找失败,调用unlazy_walk尝试drop-rcu,校验顺序锁,对最后一个可用dentry增加引用计数,若unlazy_walk成功以ref-walk模式继续查找,若不成功从头开始以ref-walk模式查找。
  • 若ref-walk查找失败,执行lookup_slow,通过inode->i_op->lookup查找。

回收流程

当dentry不在被使用,即其引用计数d_lockref变为0,或者其对应的inode被删除时,将该dentry加入superblcok的s_dentry_lru。当需要回收内存时,由prune_dcache_sb(),回收superblock中使用较少的dentry。

 

Inode Cache

inode描述文件系统中的一个文件,通常有一个或多个dentry与之对应。dentry已经完整描述了系统的目录树,并且提供了路径查找的方法,inode就不需要再处理复杂的相互关系,其管理方式也相对简单一些。

组织方式

inode中用于组织管理的数据结构如下:

struct inode {
    ...
	/* Stat data, not accessed from path walking */
	unsigned long		i_ino;    //与其i_sb一起计算hash作为在inode_hashtable中的索引
    ...
	struct hlist_node	i_hash;    //inode_hashtable全局hash表中的节点
    ...
	struct list_head	i_lru;		/* inode LRU list */    //其i_sb->s_inode_lru中的节点
    ...
}

于dentry类似,内核初始化流程中 inode_init() 函数里初始化struct kmem_cache *inode_cachep 可用于inode分配。同时该函数中还会初始化管理inode的全局hash表inode_hashtable。但通常inode结构会内嵌在文件系统私有inode结构中以实现关联,文件系统驱动通常会实现super_operations->alloc_inode()用于分配inode,此时就不会从inode_cachep中分配了。

inode结构主要有两种管理方式:

  • inode_hashtable:以superblock和i_ino作为索引在inode全局hash表中管理,主要用于通过i_ino查找inode。
  • superblock->s_inode_lru:与s_dentry_lru类似,管理不再使用的inode,当需要回收内存是,从中选取“最少使用的”inode回收。

回收流程

与dentry类似,当需要回收内存时,由prune_icache_sb(),回收superblock中使用较少的inode。

 

Page Cache

page cache是用于缓存磁盘上数据的内存页,是文件系统cache中占比最大的部分,也是对系统文件系统读写性能提升最大的部分,内核围绕page cache设计了复杂的机制用于加速文件系统的操作。 

数据结构

address_space

struct address_space {
	struct inode		*host;    //该段cache的拥有者,可以是inode或者block device
	struct xarray		i_pages;    //page列表(旧版本内核使用radix tree管理page)
    gfp_t			gfp_mask;    //申请内存时使用的flag
    ...
	struct rb_root_cached	i_mmap;    //mmap使用的vma rbtree根节点
	struct rw_semaphore	i_mmap_rwsem;
	unsigned long		nrpages;        //包含多少个page
    ...
	const struct address_space_operations *a_ops;    //address_space方法集合
    ...
}

之前介绍过这个结构,其中包含了一个pagecache列表,以及所映射的对象,这个对象可以是一个inode或者是块设备,这里主要讨论映射inode的情况。address_space内嵌在inode结构中(inode.idata),为方便使用inode初始化时使用一个指针指向它(inode.i_mapping)。a_ops方法集合提供了对page的操作方法,需要文件系统驱动实现。总的来说,address_space结构将一个inode与它在内存中的pagecache关联起来,并提供了用于操作这些page的接口,他包含了对一个inode内容进行读写需要的全部信息。

page

struct page {
	unsigned long flags;		/* Atomic flags, some possibly    //记录page的属性
					 * updated asynchronously */
    ...
	union {
		struct {	/* Page cache and anonymous pages */
            ...
			struct list_head lru;        //在LRU链表中的节点
			/* See page-flags.h for PAGE_MAPPING_FLAGS */
			struct address_space *mapping;    //指向所映射的结构(不一定是address_space)
			pgoff_t index;		/* Our offset within mapping. */    //在address_space中的偏移
            ...
		};
    ...
#ifdef CONFIG_MEMCG
	struct mem_cgroup *mem_cgroup;    //若使用了cgroup,该page也会加入cgroup维护的LRU中管理
#endif
    ...
}

Linux以页作为内存管理的最小单元,page结构用于描述内存中的一个物理页面,通常是4KB大小,其中包含了大量的信息。内存根据不同用途可分为多种类型,比如pagecache,进程匿名页,slab使用的内存池,dma等等,page结构中为了节省空间使用了大量union,这里主要看一下与pagecache相关的数据。pagecache与匿名内存使用同样结构:

  • lru:一个list_head用于加入LRU链表,
  • mapping:这里的mapping有可能指向两种结构,当page是匿名页时mapping指向anon_vma,其第一个bit(PAGE_MAPPING_ANON)为1,当page是pagecache时mapping指向address_space,其第一个bit(PAGE_MAPPING_ANON)为0。
  • index:当mapping指向address_space时,代码其在address_space.i_pages中的偏移。

pagevec

struct pagevec {
	unsigned char nr;    //缓存的page个数
	bool percpu_pvec_drained;
	struct page *pages[PAGEVEC_SIZE];    //page list
};

将page加入LRU需要获取锁,并发频繁地调用会影响性能。内存子系统为每个CPU分配一个pagevec结构,用于将这些要加入LRU的page缓存起来,等积攒到一定数量再统一加入LRU,以提升性能。

lruvec

enum lru_list {
	LRU_INACTIVE_ANON = LRU_BASE,    //匿名页inactive
	LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,    //匿名页activ
	LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,    //pagecache incactive
	LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,    //pagecache active
	LRU_UNEVICTABLE,    //不可回收
	NR_LRU_LISTS
};
struct lruvec {
	struct list_head		lists[NR_LRU_LISTS];    //5个链表
	struct zone_reclaim_stat	reclaim_stat;
	/* Evictions & activations on the inactive file list */
	atomic_long_t			inactive_age;
	/* Refaults at the time of last reclaim cycle */
	unsigned long			refaults;
	/* Various lruvec state flags (enum lruvec_flags) */
	unsigned long			flags;
#ifdef CONFIG_MEMCG
	struct pglist_data *pgdat;
#endif
};

每个内存node用5个LRU链表管理内存页,lruvec用于描述这些链表。内核编译时通常会默认打开CONFIG_MEMCG,此时系统中每个cgroup都会使用一个lruvec结构维护一组LRU,用于实现基于cgroup的内存管理。

组织方式

对文件进行读写时会先通过find_get_page()/find_get_entry()从address_space中根据偏移查找是否已经有缓存的page,若找不到就调用_page_cache_alloc()函数分配一个page,分配成功后由add_to_page_cache_lru()将page加入addres_space和LRU链表。

所以pagecache中的page主要有两种管理方式:

  • address_space->i_pages:page在所属的address_space中以xarry管理,page->index记录在其中的偏移,用于对文件内容的映射。
  • lruvec->lists:每个page加入LRU_ACTIVE_FILE 或LRU_INACTIVE_FILE  LRU链表,用于内存回收。

bufferIO 读写流程

pagecache主要在bufferIO流程中使用,这是Linux中最常见的IO模式,bufferIO利用pagecache作为缓存,降低读写操作对实际磁盘的访问频次以提高性能。结合前两篇内容,看一下ext4文件系统下bufferIO的流程。

read 流程图如下:

read的大致策略是,先从address_space->i_pages里找是否已有缓存的数据,若找到则直接拷贝给用户进程无需再访问磁盘,若找不到则扩充address_space->i_pages,将磁盘数据读到pagecache里,再拷贝给用户进程,这样后续如果再读同一page就不用访问磁盘。这里只列出大致流程,其具体的读取逻辑还是比较复杂的,涉及到预读的概念,后面再介绍。

write 流程图如下:

write的大致策略是,先建立文件要写的数据段在pagecache中的映射,将数据写入pagecahce后直接放回,等待后续回写被触发时在将pagecache数据写入磁盘。这样在频繁进行写操作时降低了实际操作磁盘的频次。

预读

当应用读一个文件时,发现要读的数据已经在pagecache中,就不用再去访问磁盘,可以极大提升读数据性能。为了更好的利用这一机制,内核设计了预读,通过预测算法预先将一部分即将请求的数据读到pagecache,将多次小IO转换为一次大IO,降低磁盘操作频次。

预测算法是预读策略的核心,使得读取逻辑变得十分复杂,来看下它是如何实现的。

file_ra_state结构内嵌与file结构中,用于记录一个fd最近一次的预读状态,

struct file_ra_state {
	pgoff_t start;			/* where readahead started */    //预读的起始page偏移
	unsigned int size;		/* # of readahead pages */    //预读窗口大小
	unsigned int async_size;	/* do asynchronous readahead when    
					   there are only # of pages ahead */    //用于计算PG_readahead标记
	unsigned int ra_pages;		/* Maximum readahead window */    //最大预读page数量,由/sys/block/*/queue/read_ahead_kb 设置
	unsigned int mmap_miss;		/* Cache miss stat for mmap accesses */    //mmap用到的miss计数
	loff_t prev_pos;		/* Cache last read() position */    //前一次读的数据偏移
}

内核注释中描述了readahead的设计思路:

 *
 *                        |<----- async_size ---------|
 *     |------------------- size -------------------->|
 *     |==================#===========================|
 *     ^start             ^page marked with PG_readahead
 *
 * To overlap application thinking time and disk I/O time, we do
 * `readahead pipelining': Do not wait until the application consumed all
 * readahead pages and stalled on the missing page at readahead_index;
 * Instead, submit an asynchronous readahead I/O as soon as there are
 * only async_size pages left in the readahead window. Normally async_size
 * will be equal to size, for maximum pipelining.
 *

预读策略使用了流水线的设计方式,当在pagecache找不到请求页面时,启动同步预读,读取一批数据并标记一个page为PG_readahead。随着应用连续读取数据,预读的page慢慢消耗,此时内核不会等到应用消耗完所有预读的page再启动新一次预读,当应用读取到PG_readahead标志的page时,就启动异步预读,读取下一批数据,将预读窗口后移。

再来看下相关代码逻辑:

page_cache_sync_readahead(),同步预读。判断该文件是否支持预读,是否有IO拥塞,然后调用ondemand_readahead。

page_cache_async_readahead(),异步预读。与同步类似,主要区别在于调用ondemand_readahead时设置hit_readahead_marker参数为true

ondemand_readahead(),基于file_ra_state实现预测算法。确定最终读取的起点与数量,调用__do_page_cache_readahead()读取page。

策略如下:

  • 读文件第一个page,一次读数据超过read_ahead_kb,之前的顺序读被中断且探测到新的顺序读,在这三种情况下初始化file_ra_state 开始一轮新的预读。
  • 探测到有持续连续读,逐步增大预读page数,将预读window后移。
  • 其他情况判定为随机读,不改变file_ra_state状态,仅读取请求大小的page。
static unsigned long
ondemand_readahead(struct address_space *mapping,
		   struct file_ra_state *ra, struct file *filp,
		   bool hit_readahead_marker, pgoff_t offset,
		   unsigned long req_size)
{
	struct backing_dev_info *bdi = inode_to_bdi(mapping->host);
	unsigned long max_pages = ra->ra_pages;
	unsigned long add_pages;
	pgoff_t prev_offset;

	/*
	 * If the request exceeds the readahead window, allow the read to
	 * be up to the optimal hardware IO size
	 */
    /*
     * 确定预读的最大page数,默认设置为read_ahead_kb相应page,若请求数据大于read_ahead_kb,        
     * 尽量使一次预读不超过硬件最大IO大小(通常在/sys/block/*/queue/max_sectors_kb设置)。
     */
	if (req_size > max_pages && bdi->io_pages > max_pages)
		max_pages = min(req_size, bdi->io_pages);

	/*
	 * start of file
	 */
	if (!offset)    //读一个文件第一页时,初始化fd的file_ra_state结构
		goto initial_readahead;

	/*
	 * It's the expected callback offset, assume sequential access.
	 * Ramp up sizes, and push forward the readahead window.
	 */
    //读取预读window末尾的下一个page,判定为顺序读
	if ((offset == (ra->start + ra->size - ra->async_size) ||
	     offset == (ra->start + ra->size))) {
		ra->start += ra->size;
		ra->size = get_next_ra_size(ra, max_pages);    //逐步增大预读page数
		ra->async_size = ra->size;
		goto readit;
	}

	/*
	 * Hit a marked page without valid readahead state.
	 * E.g. interleaved reads.
	 * Query the pagecache for async_size, which normally equals to
	 * readahead size. Ramp it up and use it as the new readahead size.
	 */
	if (hit_readahead_marker) {    //读取到PG_readahead的page时启动异步预读
		pgoff_t start;

		rcu_read_lock();
        //从offset开始,找下一个未在cache中的page作为下一次预读window的起始page
		start = page_cache_next_miss(mapping, offset + 1, max_pages);
		rcu_read_unlock();

		if (!start || start - offset > max_pages)
			return 0;

		ra->start = start;
		ra->size = start - offset;	/* old async_size */
		ra->size += req_size;
		ra->size = get_next_ra_size(ra, max_pages);    //逐步增大预读page数
		ra->async_size = ra->size;
		goto readit;
	}

	/*
	 * oversize read
	 */
    //一次性读取大量page,直接开始新一轮预读
	if (req_size > max_pages)
		goto initial_readahead;

	/*
	 * sequential cache miss
	 * trivial case: (offset - prev_offset) == 1
	 * unaligned reads: (offset - prev_offset) == 0
	 */
    //探测到有新的顺序读,开始新一轮预读
	prev_offset = (unsigned long long)ra->prev_pos >> PAGE_SHIFT;
	if (offset - prev_offset <= 1UL)
		goto initial_readahead;

	/*
	 * Query the page cache and look for the traces(cached history pages)
	 * that a sequential stream would leave behind.
	 */
    //根据offset之前的page在cache里的情况判断是否是顺序读
	if (try_context_readahead(mapping, ra, offset, req_size, max_pages))
		goto readit;

	/*
	 * standalone, small random read
	 * Read as is, and do not pollute the readahead state.
	 */
    //判断为随机读,仅读取请求大小的page,不改变file_ra_state和PG_readahead状态
	return __do_page_cache_readahead(mapping, filp, offset, req_size, 0);

initial_readahead:
    //初始化file_ra_state,根据请求大小设置预读大小及PG_readahead的位置
	ra->start = offset;
	ra->size = get_init_ra_size(req_size, max_pages);
	ra->async_size = ra->size > req_size ? ra->size - req_size : ra->size;

readit:
	/*
	 * Will this read hit the readahead marker made by itself?
	 * If so, trigger the readahead marker hit now, and merge
	 * the resulted next readahead window into the current one.
	 * Take care of maximum IO pages as above.
	 */
	if (offset == ra->start && ra->size == ra->async_size) {
		add_pages = get_next_ra_size(ra, max_pages);
		if (ra->size + add_pages <= max_pages) {
			ra->async_size = add_pages;
			ra->size += add_pages;
		} else {
			ra->size = max_pages;
			ra->async_size = max_pages >> 1;
		}
	}
    //根据file_ra_state调用__do_page_cache_readahead()读取相应page,设置(start+size-async_size)的page为PG_readahead
	return ra_submit(ra, mapping, filp);
}

__do_page_cache_readahead(),从磁盘读取一段连续page。在address_space中分配page,读取磁盘相应数据,标记PG_readahead page,并将读取的page加入LRU链表。

generic_file_buffered_read(),从文件中读取一段连续数据。在address_space中逐页查找数据对应的page,若查找不到调用page_cache_sync_readahead()进行同步预读,若查找到的page是PG_readahead调用page_cache_async_readahead()进行异步预读,找到page后校验page状态,拷贝数据。

总的来说,预读是通过探测应用的顺序读行为,预先读取数据,其探测算法的准确性对优化效果至关重要。其算法对顺序读的判定比较宽松,一些小范围的随机不会中断预读,使得如多线程对同一文件的交替顺序读的场景也会受到预读的优化,但这通常也是造成IO放大的原因。

回收流程

pagecache的回收会用LRU链表,与其他cache的回收策略类似,根据LRU算法将“最少使用”的page放在链表尾部,当触发pagecache回收时从尾部进行回收。

 

总结

本篇主要介绍了文件系统中主要用到的三种cache结构,它们的用途,组织方式,回收策略,以及内核围绕这些cache设计的机制。涉及到的流程及代码繁多,难免有些发散,部分流程也没有深入分析,比如内存回收,pagecache与swap的关联,LRU算法等等,待后续有机会做专题研究。

在讨论pagecache时,描述了bufferIO的流程,这是Linux中最常用的IO模式,它通过pagecache对读写操作加速,但同时也带来了问题。比如在读数据时需要一次额外的内存拷贝,写数据时应用不知道何时数据才能落盘,除非显式调用sync,为了让应用程序对IO操作有更多的控制,除了bufferIO外内核还提供了多种IO模式,最后一篇来梳理下这部分,文件系统为应用层提供了哪些IO模式。

 

参考资料:

https://www.ibm.com/developerworks/cn/linux/l-cache/

http://blog.chinaunix.net/uid-20522771-id-4419703.html

http://www.ilinuxkernel.com/files/Linux.Kernel.Cache.pdf

https://blog.csdn.net/zhoutaopower/article/details/93429620

Documentation/filesystems/path-lookup.txt

Documentation/filesystems/vfs.txt

热门文章

暂无图片
编程学习 ·

Android运行Linux程序

安卓直接运行arm-linux-gnueabi-gcc编译的标准嵌入式Linux程序,我们有时不想把原Linux程序重新开发一遍。第一步,给adb root权限运行,否则拷贝会提示无权限failed to copy E:\share\a8Agent1.0.1\a8Agent to /data/a8Agent: Permission deniedadb root 第二不,发送程序到安…
暂无图片
编程学习 ·

Python使用Request库实现PC端学小易(适用app版本1.0.6)

Python使用Request库实现PC端学小易app(适用app版本1.0.6)前言抓包登录操作抓包搜题操作抓包数据分析登录搜题重点代码实现导入库tkinter实现简易图形界面部分request库实现登录部分搜题部分整理输出至tkinter部分完整代码重点 前言 一直以来学小易只有安卓段与IOS端的app,在…
暂无图片
编程学习 ·

SSM整合小案例

SSM整合 数据库部分(Oracle)创建表 CREATE TABLE product( id varchar2(32) default SYS_GUID() PRIMARY KEY, productNum VARCHAR2(50) NOT NULL, productName VARCHAR2(50), cityName VARCHAR2(50), DepartureTime timestamp, productPrice Number, productDesc VARCHAR2(500…
暂无图片
编程学习 ·

02 | 该如何选择消息队列?

1.应用场景见: https://blog.csdn.net/william_n/article/details/1040254082.学习/操作2.1 阅读文档02 | 该如何选择消息队列?李玥 2020-01-1400:0013:59讲述:李玥 大小:12.81M你好,我是李玥。这节课我们来聊一下几个比较常见的开源的消息队列中间件。如果你正在做消息队…
暂无图片
编程学习 ·

NC6 基于元数据的持久化服务接口实现类

基于元数据的持久化服务接口实现类: package nc.md.persist.framework.imp;import java.util.Collection;import nc.md.data.access.NCObject; import nc.md.data.criterion.QueryCondition; import nc.md.model.MetaDataException; import nc.md.persist.framework.IMDPersis…
暂无图片
编程学习 ·

Web服务器防护技术你了解多少?

技术的迅速发展,给人们提供便利的同时,也给人们带来了威胁。通常情况下,黑客、病毒会利用系统的漏洞来进行网络攻击,如篡改网页、蔓延病毒等,从而造成用户信息的窃取、重要数据的破坏。因此,要对web服务器的安全问题引起足够的重视,要加大安全防护力度、构建安全防护系统…
暂无图片
编程学习 ·

使用john软件进行账户弱口令检测实验

使用john软件进行账户弱口令检测实验 前言 在生产环境中,服务器账号的密码能够不被黑客入侵破解是尤为重要的,关系着业务正常运行的安全,所以在创建完账户的密码后,我们需要进行弱口令的检测,排查出是否有容易被破解的密码存在。 本次实验使用的破解密码软件是john-1.8.0版…
暂无图片
编程学习 ·

考研初试备考,感谢曾经努力的自己

人生的第一篇博客,说晚不晚,以后我会好好学习IT技术,并腾出时间写写东西,想写这篇文章有很长时间了,当初写好了也没有发布,现在作为一个过来人润色一番,着手发布。找不到合适的平台、合适的时间、合适的场地、合适的心情以及合适的内容,现在才开始在CSDN动手写东西,然…
暂无图片
编程学习 ·

结构体学生信息输入

不知不觉学到第七章结构体了,这一章开始到后面的章节网上的免费课程就越来越少了。每次有不会的只能各种百度,心累。。。但还是会坚持的!!! 记录第7章课后习题第3题: 题目:编写一个函数print,打印一个学生的成绩数组,该数组中有5个学生的数据,每个学生的数据包括num(学…
暂无图片
编程学习 ·

kafka事务transactional.id相关

https://www.cnblogs.com/jingangtx/p/11330338.htmlhttps://blog.csdn.net/oTengYue/article/details/104727512/看了这两篇关于事务id的资料,我有些疑问:两篇文章里都讲了transactional.id是用户自己设置的,而且transactional.id与producerId在事务管理器中是一一对应关系…
暂无图片
编程学习 ·

spring @Primary-@Qualifier在spring中的使用

在spring 中使用注解,常使用@Autowired, 默认是根据类型Type来自动注入的。但有些特殊情况,对同一个接口,可能会有几种不同的实现类,而默认只会采取其中一种的情况下 @Primary 的作用就出来了。下面是个简单的使用例子。 有如下一个接口 public interface Singer {String …
暂无图片
编程学习 ·

web编程期末大作业 项目一

作业要求一、登录注册页面 检测用户名密码是否一致,是则跳转至新闻页面增加注册用户登录界面如下: 发现自己实在是不太了解bootstrap的各种样式,界面也不知道该怎么优化,后面的html文档的css就都用的老师给的模板了 -_-||二、操作记录保留 首先先在数库中建立两张表,一个存…
暂无图片
编程学习 ·

当你忘记网站上的密码时怎么办?Python如何快速帮你找回?

前言本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理。现如今浏览器可谓是五花八门,火狐、UC、360、QQ 这些浏览器不论美观还是所谓的安全方面都做的很符合我们需求。但如果你的工作与 IT 挂钩,无疑 Chr…
暂无图片
编程学习 ·

as 找不到调试设备(手机,虚拟机)

在flutter sdk 路径下执行命令flutter config --android-sdk D:\envi\android\android-sdk(你的android sdk路径)我在git bash 中执行之后没反应,应该是git bash环境语法设置有问题,于是改成 flutter config --android-sdk D:\\envi\\android\\android-sdk可以鸟
暂无图片
编程学习 ·

《MySQL数据库》常用函数整理

以下内容,是我整理出来的比较常用的字符串函数,数值函数,日期函数。第一类:字符串函数1、conv(n,from_base,to_base):对from_base进制的数n,转成to_base进制的表示方式(PS:进制范围为2-36进制,当to_base是负数时,n作为有符号数否则作无符号数)mysql> select conv(&quo…
暂无图片
编程学习 ·

公司开发规范 - 【管理岗的第二年】

由于阿里的规范太长了 本文就不赘述阿里大佬了,只是用于我带的团队书写代码严格遵循驼峰命名规范每个方法类【不包含工具类、抽象类、实体类】行数不能超过200行,可以拆解到多个类(DataOperatorService,DataLogService等)每个方法有效逻辑行数【不包含注释、静态、属性字段…