深度理解文件系统 ---- 从磁盘存储到内核存储

目录

理解磁盘

磁盘的物理结构

磁盘的存储结构

CHS寻址

磁盘的逻辑结构

[CHS地址 和 LBA地址 的转化](#CHS地址 和 LBA地址 的转化)

理解文件系统的前置知识

引入"块"概念

引入"分区"概念

磁盘分区的全过程

两种分区格式

磁盘分区步骤

[引入"inode" 概念](#引入"inode" 概念)

文件系统

[Block Group](#Block Group)

[Super Block](#Super Block)

GDT

[Block Bitmap](#Block Bitmap)

[Inode Bitmap](#Inode Bitmap)

[Inode Table](#Inode Table)

[Data Block](#Data Block)

目录与文件名

路径解析

路径缓存

挂载分区

软硬链接

硬链接

软链接


理解磁盘

磁盘的物理结构

磁盘的存储结构

扇区:磁盘存储数据的基本单位,也是磁盘读取和写入数据的最小单位,编号是从 1 开始的,大小为512字节。

磁头:每个盘片一般有上下两面,分别对应一个磁头。

磁道:有一圈同半径的扇区组成,每一个磁道的扇区数都相同。磁道是从盘片外圈往内圈进行编号,编号从 0 开始,靠近主轴的同心圆用于停靠磁头,不存储数据。

柱面:由多个距离中心轴同半径磁道构成柱面, 编号是从 0 开始的。

磁盘容量:磁头数 * 磁道数 * 每个磁道的扇区数 * 每个扇区的字节数

注意:传动臂上的磁头是共进退的。

CHS寻址

磁盘上是如何定位扇区的?

定位柱面(cylinder) -> 定位磁头(head) -> 定位扇区(sector) (简称CHS寻址方式)

文件 = 内容 + 属性 都是磁盘上的数据,存储在磁盘的扇区上,能定位一个扇区,就能定位多个扇区,从而找到文件在磁盘上所存储的位置。

CHS寻址方式对于早期的磁盘非常有效。但CHS寻址模式支持的磁盘容量有限,因为系统用 8 bit 来存储磁头地址,用 10 bit 来存储柱面地址, 用 6 bit来存储扇区地址,而一个扇区共有512字节,这样使用CHS寻址方式得到一块磁盘的最大容量为 256 * 1024 * 63 * 512 B = 8064 MB,导致磁盘的容量太小。

磁盘的逻辑结构

抽象理解

虽然磁盘现实中是环状的,但逻辑上我们可以把磁盘想象成卷在一起的磁带,那么我们将磁带展开,磁盘的逻辑结构就可以类似于线性结构。然后对这个线性结构进行编号,这样对于每一个扇区,就有了一个线性地址(其实就是数组下标),这种地址叫做 LBA(Logical Block Address)。

真实理解

对于某一个磁道展开,其实就是一个一维数组。

整个磁盘所有盘面的同一个磁道展开,即将一个柱面展开,其实就是一个二维数组。

对于整个磁盘的所有柱面进行展开,其实就是一个三维数组。

对于学习语言的时候,数组在物理结构上是连续的,那么三维数组就可以看作一个在物理结构上连续的一维数组,对应每个数组元素(扇区)都会存在对应的下标,这个下标被叫做 LBA地址,其实就是线性地址。

对于现代的操作系统,统一使用LBA地址,不再使用CHS地址了。但是会存在一些老主板BIOS、老BIOS程序、老固件依旧在使用CHS,(因为厂商不敢直接删掉CHS,一删老电脑全部无法开机读盘),而磁盘始终只认识LBA地址。所以肯定会存在 CHS地址和LBA地址的转化。

CHS地址 和 LBA地址 的转化

CHS地址和LBA地址的转化是由 BIOS完成的。

CHS 转成 LBA:

磁头数 * 每个磁道的扇区数 = 单个柱面的扇区总数

LBA = 柱面号 * 单个柱面的扇区总数 + 磁头号 * 每个磁道的扇区数 + 扇区号 -1

注意:扇区号通常在CHS中是从1开始编号的,而在LBA中,地址从0开始编号的。柱面和磁道都是从0开始编号的。总柱面,磁道个数,扇区总数等信息,在磁盘内部会自动维护,BIOS读取磁盘获取数据,进行填柱面,磁头,扇区,通过上述公式进行将LBA地址发给磁盘(之前CHS年代,没有成熟的操作系统,计算机的运行由BIOS全权控制)。

LBA 转成 CHS:

柱面号 = LBA / (磁头数 * 每个磁道的扇区数)

磁头号 = (LBA % (磁头数 * 每个磁道的扇区数)) / 每个磁道的扇区数

扇区号 = (LBA % 每个磁道的扇区数) + 1

总之:现代的操作系统和磁盘只使用LBA地址,不认识CHS地址,操作系统就可以通过LBA地址在磁盘进行读取或写入数据。

理解文件系统的前置知识

引入"块"概念

磁盘就是典型的"块"设备,操作系统读取磁盘数据的时候,不是一个个扇区地读取,这样效率太低,而是一次性读取多个扇区,即一次性读取一个"块"。

磁盘的存储空间是被划分为一个个的"块"。一个"块"的大小是由格式化的时候确定的,并且不可以更改,最常见的一个"块"的大小为4KB,即连续八个扇区组成一个"块"。"块"是文件系统读取和写入数据的最小单位。

注意:磁盘本质可以看作成一个"一维数组",数组下标就是LBA,每个元素都是一个扇区。

每个扇区都有LBA,那么8个扇区为1个块,对于每一个块的起始编号我们也能算出来(该块的起始编号 = LBA / 8),知道块号通过 LAB = 块号 * 8 + n(n是块内第几个扇区),也就能算出LAB。

引入"分区"概念

磁盘分区的全过程

对于磁盘,是可以被分成多个分区的,以Windows的视角来看,一块磁盘可能被分成C、D、E盘,这些盘就是一个个分区。

分区过程:对于整块磁盘出厂是一整片空白扇区,没有任何划分,分区本质:在磁盘头部写入分区表,把整块盘切出一块块区域。

两种分区格式

  1. MBR分区(老式)

磁盘前512字节 = 主引导记录 MBR

前446 字节 = 开机引导代码

中间64字节 = 分区表

最后2字节 = 启动标志

2.GPT分区(现代主流)

在老式的基础上,分区表多份备份,更安全,磁盘容量更大,分区数量多

磁盘分区步骤

1.扫描整块磁盘

识别磁盘所有扇区,确定总容量

2.划分分区边界

手动/工具指定:每个分区起始扇区、结束扇区

3.写入分区表

把每个分区的起止位置,分区类型,存放在磁盘分区表中

4.格式化(后面说)

给分区写入文件系统,让操作系统能识别该分区的文件系统

5.挂载(后面说)

Linux把分区绑定到某个目录,在查找文件的inode时,就能定位分区进行查找。

引入"inode" 概念

ls 和 stat 指令都能够查看文件的属性(元数据),而文件数据都存在"块"中,那么磁盘上肯定存在一个区域存储文件的属性,这种存储文件属性的区域就叫做inode(本质就是一个数据结构)。

每一个文件都有对应的inode,inode里面存储着文件的属性。通过 ls -li 可以查看到文件的inode的编号(inode编号:inode在Inode Table 中的数组下标)

注意:

Linux下文件属性和文件内容的存储是分离的

Linux下,保存文件属性的集合叫做inode,一个文件对应一个inode

文件系统层面上的inode

cpp 复制代码
/*
 * Structure of an inode on the disk
 */
struct ext2_inode {
	__le16	i_mode;		/* File mode */
	__le16	i_uid;		/* Low 16 bits of Owner Uid */
	__le32	i_size;		/* Size in bytes */
	__le32	i_atime;	/* Access time */
	__le32	i_ctime;	/* Creation time */
	__le32	i_mtime;	/* Modification time */
	__le32	i_dtime;	/* Deletion Time */
	__le16	i_gid;		/* Low 16 bits of Group Id */
	__le16	i_links_count;	/* Links count */
	__le32	i_blocks;	/* Blocks count */
	__le32	i_flags;	/* File flags */
	union {
		struct {
			__le32  l_i_reserved1;
		} linux1;
		struct {
			__le32  h_i_translator;
		} hurd1;
		struct {
			__le32  m_i_reserved1;
		} masix1;
	} osd1;				/* OS dependent 1 */
	__le32	i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
	__le32	i_generation;	/* File version (for NFS) */
	__le32	i_file_acl;	/* File ACL */
	__le32	i_dir_acl;	/* Directory ACL */
	__le32	i_faddr;	/* Fragment address */
	union {
		struct {
			__u8	l_i_frag;	/* Fragment number */
			__u8	l_i_fsize;	/* Fragment size */
			__u16	i_pad1;
			__le16	l_i_uid_high;	/* these 2 fields    */
			__le16	l_i_gid_high;	/* were reserved2[0] */
			__u32	l_i_reserved2;
		} linux2;
		struct {
			__u8	h_i_frag;	/* Fragment number */
			__u8	h_i_fsize;	/* Fragment size */
			__le16	h_i_mode_high;
			__le16	h_i_uid_high;
			__le16	h_i_gid_high;
			__le32	h_i_author;
		} hurd2;
		struct {
			__u8	m_i_frag;	/* Fragment number */
			__u8	m_i_fsize;	/* Fragment size */
			__u16	m_pad1;
			__u32	m_i_reserved2[2];
		} masix2;
	} osd2;				/* OS dependent 2 */
};

内核层面上的inode

cpp 复制代码
struct inode {
	struct hlist_node	i_hash;
	struct list_head	i_list;
	struct list_head	i_sb_list;
	struct list_head	i_dentry;
	unsigned long		i_ino;
	atomic_t		i_count;
	umode_t			i_mode;
	unsigned int		i_nlink;
	uid_t			i_uid;
	gid_t			i_gid;
	dev_t			i_rdev;
	loff_t			i_size;
	struct timespec		i_atime;
	struct timespec		i_mtime;
	struct timespec		i_ctime;
	unsigned int		i_blkbits;
	unsigned long		i_blksize;
	unsigned long		i_version;
	unsigned long		i_blocks;
	unsigned short          i_bytes;
	unsigned char		i_sock;
	spinlock_t		i_lock;	/* i_blocks, i_bytes, maybe i_size */
	struct semaphore	i_sem;
	struct rw_semaphore	i_alloc_sem;
	struct inode_operations	*i_op;
	struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
	struct super_block	*i_sb;
	struct file_lock	*i_flock;
	struct address_space	*i_mapping;
	struct address_space	i_data;
#ifdef CONFIG_QUOTA
	struct dquot		*i_dquot[MAXQUOTAS];
#endif
	/* These three should probably be a union */
	struct list_head	i_devices;
	struct pipe_inode_info	*i_pipe;
	struct block_device	*i_bdev;
	struct cdev		*i_cdev;
	int			i_cindex;

	__u32			i_generation;

#ifdef CONFIG_DNOTIFY
	unsigned long		i_dnotify_mask; /* Directory notify events */
	struct dnotify_struct	*i_dnotify; /* for directory notifications */
#endif

	unsigned long		i_state;
	unsigned long		dirtied_when;	/* jiffies of first dirtying */

	unsigned int		i_flags;

	atomic_t		i_writecount;
	void			*i_security;
	union {
		void		*generic_ip;
	} u;
#ifdef __NEED_I_SIZE_ORDERED
	seqcount_t		i_size_seqcount;
#endif
};

注意:

inode编号没有在文件系统的inode中,在内核级的inode中

文件名属性没有在inode(文件系统)数据结构中

inode(文件系统)的大小一般为128字节或者256字节

任何文件的内容大小可以不同,但是属性大小一定相同(因为inode(文件系统)中存在一个固定大小的数组,里面存放着每一个数据块的指针,每一个数据块中存放着文件的内容)

到目前为止,我们已经知道磁盘是"块"设备,操作系统读取磁盘数据的时候,读取的基本单位是"块"。而"块"又是磁盘的每个分区下的结构,那么"块"在分区上是怎么分布的,操作系统是如何找到"块"的呢,inode又存放在分区的哪个位置呢?

对于上述所有问题,文件系统将为你揭晓答案。

文件系统

文件系统是有很多种类的,例如:ext2、ext3、ext4、XFS等,以下内容皆以ext2文件系统进行讲述,理解了一个文件系统,进而就能理解了其他文件系统。

对于我们在磁盘上存储文件,那么必须先把磁盘格式化为某种格式的文件系统,才能存储文件。文件系统的目的就是组织和管理磁盘上的文件。

注意:文件系统是以分区为载体的。

ext2 文件系统会将整个分区划分成若干个同样大小的块组进行管理。只要能管理好每一个组就能管理好所有组,从而管理好一个分区,从而管理好整个磁盘。

上图中的MBR有一个分区表,存储着每一个分区的起止地址来划分磁盘。

上图中Boot Sector的大小是确定的,为1KB,用来存储磁盘分区的信息和启动信息,任何文件系统都不能修改启动块。

Block Group

ext2 文件系统会根据分区的大小将其划分为数个Block Group。每一个Block Group都有着相同的结构组成。

Super Block

存放文件系统本身的结构信息,描述整个分区的文件系统信息

cpp 复制代码
/*
 * Structure of the super block
 */
struct ext2_super_block {
	__le32	s_inodes_count;		/* Inodes count */
	__le32	s_blocks_count;		/* Blocks count */
	__le32	s_r_blocks_count;	/* Reserved blocks count */
	__le32	s_free_blocks_count;	/* Free blocks count */
	__le32	s_free_inodes_count;	/* Free inodes count */
	__le32	s_first_data_block;	/* First Data Block */
	__le32	s_log_block_size;	/* Block size */
	__le32	s_log_frag_size;	/* Fragment size */
	__le32	s_blocks_per_group;	/* # Blocks per group */
	__le32	s_frags_per_group;	/* # Fragments per group */
	__le32	s_inodes_per_group;	/* # Inodes per group */
	__le32	s_mtime;		/* Mount time */
	__le32	s_wtime;		/* Write time */
	__le16	s_mnt_count;		/* Mount count */
	__le16	s_max_mnt_count;	/* Maximal mount count */
	__le16	s_magic;		/* Magic signature */
	__le16	s_state;		/* File system state */
	__le16	s_errors;		/* Behaviour when detecting errors */
	__le16	s_minor_rev_level; 	/* minor revision level */
	__le32	s_lastcheck;		/* time of last check */
	__le32	s_checkinterval;	/* max. time between checks */
	__le32	s_creator_os;		/* OS */
	__le32	s_rev_level;		/* Revision level */
	__le16	s_def_resuid;		/* Default uid for reserved blocks */
	__le16	s_def_resgid;		/* Default gid for reserved blocks */
	/*
	 * These fields are for EXT2_DYNAMIC_REV superblocks only.
	 *
	 * Note: the difference between the compatible feature set and
	 * the incompatible feature set is that if there is a bit set
	 * in the incompatible feature set that the kernel doesn't
	 * know about, it should refuse to mount the filesystem.
	 * 
	 * e2fsck's requirements are more strict; if it doesn't know
	 * about a feature in either the compatible or incompatible
	 * feature set, it must abort and not try to meddle with
	 * things it doesn't understand...
	 */
	__le32	s_first_ino; 		/* First non-reserved inode */
	__le16   s_inode_size; 		/* size of inode structure */
	__le16	s_block_group_nr; 	/* block group # of this superblock */
	__le32	s_feature_compat; 	/* compatible feature set */
	__le32	s_feature_incompat; 	/* incompatible feature set */
	__le32	s_feature_ro_compat; 	/* readonly-compatible feature set */
	__u8	s_uuid[16];		/* 128-bit uuid for volume */
	char	s_volume_name[16]; 	/* volume name */
	char	s_last_mounted[64]; 	/* directory where last mounted */
	__le32	s_algorithm_usage_bitmap; /* For compression */
	/*
	 * Performance hints.  Directory preallocation should only
	 * happen if the EXT2_COMPAT_PREALLOC flag is on.
	 */
	__u8	s_prealloc_blocks;	/* Nr of blocks to try to preallocate*/
	__u8	s_prealloc_dir_blocks;	/* Nr to preallocate for dirs */
	__u16	s_padding1;
	/*
	 * Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set.
	 */
	__u8	s_journal_uuid[16];	/* uuid of journal superblock */
	__u32	s_journal_inum;		/* inode number of journal file */
	__u32	s_journal_dev;		/* device number of journal file */
	__u32	s_last_orphan;		/* start of list of inodes to delete */
	__u32	s_hash_seed[4];		/* HTREE hash seed */
	__u8	s_def_hash_version;	/* Default hash version to use */
	__u8	s_reserved_char_pad;
	__u16	s_reserved_word_pad;
	__le32	s_default_mount_opts;
 	__le32	s_first_meta_bg; 	/* First metablock block group */
	__u32	s_reserved[190];	/* Padding to the end of the block */
};

Super Block 的信息如果被破坏,那么整个文件系统结构就被破坏了。为了保证文件系统在磁盘部分扇区出现物理问题而导致Super Block的信息出现问题还能正常工作,那么一个文件系统的Super Block的信息会在多个 Block Group进行备份,使得一个文件系统更健壮。

即:不是每一个Block Group 都有Super Block,而是Super Block 在多个 Block Group中。

GDT

块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储一个块组 的描述信息,如在这个块组中从哪里开始是inode Table,从哪里开始是Data

Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有一份拷贝。​

Block Bitmap

Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用​。

Inode Bitmap

Inode Bitmap中记录着Inode Table中哪个inode块已经被占用,哪个inode块没有被占用​。

Inode Table

存放文件属性,当前分组所有inode属性的集合。

inode编号以分区为单位进行编号,可以跨组,不可以跨分区。

Data Block

数据区:存放文件内容,也就是一个一个的块。

对于普通文件,文件的数据存储在数据块中。

对于目录,该目录下的所有文件名和目录名以及它们的inode编号存储在所在目录的数据块中。

Data Block 编号以分区为单位进行编号,可以跨组,不可以跨分区。

目录与文件名

对于我们在Linux系统中访问文件,都是用的文件名,没用过inode编号啊?

目录在磁盘上是怎么存储的?

对于磁盘,磁盘没有目录的概念,只有文件属性 + 文件内容的概念。那么目录在磁盘上存储的方式就和文件一摸一样,inode块中存放属性,数据块中存放文件名和inode编号的映射关系。

所以,访问一个文件,必须找到当前文件的目录,根据当前目录的内容,即文件名和inode编号的映射关系,在文件系统的Inode Table中找到对应的inode,就找到了文件的属性和内容。

路径解析

路径解析:对于访问一个文件,从根目录开始,根据文件名和inode的映射关系,依次打开每一个目录,从而得到目标文件的inode。

对于/home/lt/code/test.c 这样的路径,就是从 / 目录开始进行解析得到home的inode,在home的内容中得到code的inode,在code的内容中得到test.c的inode。

所以,访问文件必须要有目录 + 文件名 = 路径的原因。

根目录固定文件名,inode号,无需查找,系统开机之后就已经知道。

路径是提供?

访问文件时,都是指令/工具访问,本质是进程访问,进程有CWD,进程会提供路径。

open文件,用户提供路径。

创建一个新文件:

1.存储属性 在Inode Bitmap找到一个空闲位置,在Inode Table中建立inode存储文件的属性

2.存储数据 在Block Bitmap 找到空闲位置,把找到的地址交给inode然后将内核态缓冲区的内容拷贝到对应Data Block

3.添加文件名到当前目录 在Block Bitmap找到空闲位置,把找到的地址交给目录的inode,将文件名和inode的映射关系拷贝到对应Data Block

路径缓存

访问任何文件,都要从 / 目录开始进行路径解析吗?

原则上是,但是这样做就会不断地和磁盘进行交互,不断地进行IO,对于操作系统来说,这样做太慢了,所以Linux在内核中维护一个树状路径结构,来记录每一个访问过的目录中的所有文件(不会记录 . 和 ..),进行缓存历史路径结构。

那么Linux中目录的概念油然而生,如果打开的文件是目录,OS在内存中进行路径缓存,就形成了树形结构,每一个父节点就会有多个子节点,构成了内核级的目录。

Linux内核中,维护树状路径结构的内核结构体叫做:struct dentry

struct dentry {

atomic_t d_count;

unsigned int d_flags; /* protected by d_lock */

spinlock_t d_lock; /* per dentry lock */
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */

/*

* The next three fields are touched by __d_lookup. Place them here

* so they all fit in a cache line.

*/

struct hlist_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct list_head d_lru; /* LRU list */

/*

* d_child and d_rcu can share memory

*/

union {

struct list_head d_child; /* child of parent list */

struct rcu_head d_rcu;

} d_u;
struct list_head d_subdirs; /* our children */

struct list_head d_alias; /* inode alias list */

unsigned long d_time; /* used by d_revalidate */

struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */

void *d_fsdata; /* fs-specific data */

#ifdef CONFIG_PROFILING

struct dcookie_struct *d_cookie; /* cookie, if any */

#endif

int d_mounted;

unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */

};

对于打开的每一个文件(包括普通文件),在内存中都有着其对应的dentry结构,就可以在内存中形成整个树形结构。

整个树形节点也同时会属于LRU(Least Recently Used,最近最少使用)结构中,进行节点淘汰​。

整个树形节点也同时会属于Hash,方便快速查找​。

更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何文件,都在先在这

棵树下根据路径进行查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry

结构,缓存新路径。

挂载分区

在指定的分区中,根据路径就可以找到目标文件的inode,进而找到文件的内容和属性。

那我们思考一个问题?

Linux下有多个分区,inode编号在不同的分区中可以相同,那OS是怎么知道文件在哪个分区的?

挂载:把磁盘分区,绑定到Linux中的某个目录。

对于Linux没有C盘D盘的概念,一切皆文件,分区必须挂载到目录才能用。

可以根据访问目标文件的"路径前缀"准确判断目标文件在哪个分区中。

软硬链接

硬链接

真正找到磁盘上文件的不是文件名而是inode。在Linux中可以让多个文件名对应同一个inode,本质就是多了一组新的文件名和inode的映射关系。

两个文件名对应的inode编号完全相同,且它们都是普通文件,而它们的链接状态也完全相同,它们被称为指向文件的硬链接。

删除文件:在目录中将对应的记录删除 -> 将硬连接数 -1,如果硬连接数为0 -> 将Inode Bitmap对应的比特位改为0,将Block Bitmap对应的比特位改为0。

在Linux下,用户不能建立对目录文件硬链接,只能对普通文件建立硬链接

不能对目录文件建立硬链接的原因:Linux操作系统没有对硬链接做特殊标志,如果用户建立的硬链接具备了嵌套关系,Linux操作系统的查找文件功能以及其他功能会不断地在这两个硬链接进行查找,进而造成了死循环。

硬链接作用:

1.文件备份(未采用copy的形式,硬链接这种方法更节约磁盘空间)

  1. . 和 .. 目录也是硬链接,方便用户的使用(操作系统执行部分功能时会其做特殊处理)

软链接

硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,具有独立的inode。

软连接文件的数据块里存的内容,是目标文件的路径。

可以对目录进行建立软连接,因为软连接具有特殊的文件类型,操作系统会对其做特殊处理。

软连接作用:

类似快捷方式(也不是copy,因为软连接文件的内容是目标文件的路径)

相关推荐
2301_803934613 小时前
MySQL 字段类型选择规范指南
jvm·数据库·python
Bechamz3 小时前
大数据开发学习Day36
大数据·学习
C-20023 小时前
基于 JumpServer 容器化部署 ES 集群
大数据·elasticsearch·搜索引擎
oddsand13 小时前
Redis网络模型
java·数据库·redis
皮卡祺q3 小时前
【redies0-导论】分布式系统的演进-引进redis原因
java·数据库·redis
captain_AIouo4 小时前
降本增效突围,Captain AI助力Ozon商家提升盈利空间
大数据·人工智能·经验分享·aigc
南极企鹅4 小时前
事务&@Transactional注解
java·数据库·spring·oracle·mybatis
UrSpecial4 小时前
Redis与多线程
数据库·redis·缓存
bqq198610264 小时前
MySQL 8与MySQL 5.7的主要区别
数据库·mysql