news 2026/10/3 3:39:33

STM32存储三件套:SFUD+FAL+flashDB移植实战与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32存储三件套:SFUD+FAL+flashDB移植实战与避坑指南

先聊点实在的:STM32项目里做数据存储,大多数人一开始都会走弯路。要么直接怼一片裸的SPI Flash,应用层自己拼读写地址、自己管擦除、自己记偏移量,结果代码越写越绕;要么选个E2PROM从头到尾硬扛,容量不够改方案,掉电写坏数据查半天。我这次在STM32F407上,把FAL、SFUD、flashDB这三个组件一次接齐,存储层从硬件驱动到应用接口全部捋顺,整个过程踩了不少坑,也总结出一套可以直接照抄的移植流程。这篇文章把每个组件的角色、移植步骤、关键配置和实际踩坑经验都摊开讲,适合正在做参数存储、日志记录、OTA升级分区管理的朋友参考。


1. 先搞清楚三件套各自的定位

1.1 在没有三件套之前,我踩过的存储坑

早期做产品,我习惯直接用SPI Flash硬编码读写。比如W25Q64,芯片8MB,我先把前1MB划分给参数区,后面全放日志。听起来简单,但真做起来很痛苦:应用层要自己去算扇区偏移,要记录“上一条日志写到哪了”,一旦程序版本迭代、参数结构体变了,新旧数据兼容全靠手动迁移。

更头疼的是可靠性问题。Flash写入之前必须先擦除,而擦除以扇区为单位,W25Q64一个扇区是4KB。我早期为了改一个4字节的参数,直接把整个扇区擦掉再写回去,中途一旦断电,整块参数区直接报废。后来我试过用备份区和标志位做原子操作,代码写了几百行,逻辑还是漏洞百出。

这个阶段最大的问题不是能力不够,而是架构不对。存储这件事从上到下本来应该分层:最底层管硬件差异,中间层管分区划分,最上层管应用读写和可靠性。自己直接一把梭,等于把这三层全部耦合在一个模块里,改动一个芯片、增加一个功能,都要重写一大堆代码。

1.2 三件套的分工与协作逻辑

SFUD、FAL、flashDB这套组合,正好对应我上面说的三个层次。

SFUD(Serial Flash Universal Driver)负责最底层,把不同厂家的SPI Flash封装成统一接口。它内部维护了一个芯片型号表,W25Q64、GD25Q64这类常见芯片都能自动识别,识别之后读写擦除全走一套API,换芯片时不用改上层代码。

FAL(Flash Abstraction Layer)是做分区管理的,类似PC上的磁盘分区。它把一片物理Flash划分成多个逻辑分区,比如App区、参数区、日志区,每个分区有名字、有起始地址、有长度,上层读写全部通过分区名操作,底层地址变化完全不感知。

flashDB是跑得最上层的一个轻量级嵌入式数据库,它不直接接触硬件,而是在FAL的分区之上提供两种能力:一种是KV(键值对)存储,适合存设备参数、校准数据;另一种是TSDB(时序数据)存储,适合存日志、采集数据。掉电安全、磨损均衡、数据索引这些事,它都帮你做好了。

三者协作链路非常清晰:SFUD让物理Flash可用,FAL把Flash切成多个逻辑空间,flashDB在逻辑空间里提供数据库能力。应用层只面对flashDB的接口,底层换芯片、换分区方案,应用层代码一行都不用动。


2. 移植前的软硬件准备

2.1 硬件选型与接口确认

这次移植用的是STM32F407VET6主控,Flash芯片是W25Q64(8MB容量,4KB扇区),走SPI1接口。W25Q系列在国产和进口方案里占有率高,SFUD原生支持,参考价值最大。

接线其实没太多悬念,SPI的四个核心引脚按常规配置:SCK挂PB3,MISO挂PB4,MOSI挂PB5,CS挂PB12。这里特别注意一下,CS引脚一定要用软件控制,不能用硬件NSS自动管理,因为W25Q系列要求片选信号在指令期间全程拉低,硬件NSS的模式不好控制时序。

供电方面给W25Q64配一个100nF去耦电容,靠近VCC引脚放置,这个对稳定性影响很大。我早期测试时芯片经常“不识盘”,最后排查发现就是电源纹波太大,电容加上去问题立刻消失。

除了Flash本身,还需要一个用于调试的串口。因为SFUD和FAL本身没有可视化输出,排查问题时全靠串口日志辅助。波特率用115200,一个发送引脚就够了。

2.2 软件工程基线

软件方面,我用的开发环境是Keil MDK 5.30,标准外设库,没用HAL,原因很直接:身边团队的老项目都是标准库,迁移成本低,而且标准库的SPI读写代码直来直去,更容易看清楚时序问题。

三个组件的版本分别是:SFUD 1.1.0、FAL 0.5.0、flashDB 1.0.0。建议从官方Gitee仓库拉最新release,不要随便找别人改过的版本,我见过太多“移植失败”其实是用了一份别人魔改的代码包,问题根本没法查。

工程里需要提前建好三个文件夹:sfud、fal、flashdb,分别放对应源码。SFUD源码里src目录下放核心代码,port目录下放移植文件比如sfud_port.c,这个文件后面要重点改。FAL源码里同样有src和port目录,其中fal_flash_sfud_port.c负责把SFUD接口接入FAL,也是关键文件。

编译时要注意,Keil的C99模式必须打开,这三个组件的源码全都用到了C99特性,不打开会有大量编译报错。另外宏定义要预先加好:FDB_USING_KVDB、FDB_USING_TSDB按需开启,FDB_WRITE_GRAN按Flash的写粒度设置为1,这些细节后面展开说。


3. SFUD移植:让SPI Flash“开口说话”

3.1 关键移植文件sfud_port.c的改写

SFUD的移植工作基本都集中在sfud_port.c这个文件。它的作用就是告诉SFUD:底层的SPI是怎么调用的,读、写、擦除是怎么实现的。打开这个文件,默认会看到一个sfud_spi_init函数和若干空接口,把这些空接口填上就行。

我用标准库完成SPI1的初始化,然后封装了三个底层函数:spi_flash_read、spi_flash_write、spi_flash_erase。

static sfud_err spi_flash_read(const sfud_flash *flash, uint32_t addr, size_t size, uint8_t *data) { // 拉低CS GPIO_ResetBits(GPIOB, GPIO_Pin_12); // 发送读命令 0x03 spi_send_byte(0x03); // 发送24位地址,高字节在前 spi_send_byte((addr >> 16) & 0xFF); spi_send_byte((addr >> 8) & 0xFF); spi_send_byte(addr & 0xFF); // 连续读取size个字节 for (size_t i = 0; i < size; i++) { data[i] = spi_receive_byte(); } // 拉高CS GPIO_SetBits(GPIOB, GPIO_Pin_12); return SFUD_SUCCESS; }

写操作稍微复杂一点,因为Flash写入前必须判断目标地址状态,如果没擦除过,写进去的数据就是错的。正常的流程是先发写使能命令0x06,再发写命令0x02,然后是24位地址,最后才是要写的数据。

static sfud_err spi_flash_write(const sfud_flash *flash, uint32_t addr, size_t size, const uint8_t *data) { // 发送写使能 GPIO_ResetBits(GPIOB, GPIO_Pin_12); spi_send_byte(0x06); GPIO_SetBits(GPIOB, GPIO_Pin_12); // 发送写命令和地址 GPIO_ResetBits(GPIOB, GPIO_Pin_12); spi_send_byte(0x02); spi_send_byte((addr >> 16) & 0xFF); spi_send_byte((addr >> 8) & 0xFF); spi_send_byte(addr & 0xFF); for (size_t i = 0; i < size; i++) { spi_send_byte(data[i]); } GPIO_SetBits(GPIOB, GPIO_Pin_12); // 等待写入完成,轮询状态寄存器 wait_busy(); return SFUD_SUCCESS; }

擦除操作更简单,但需要注意,W25Q64支持三种擦除粒度:整片擦除、64KB块擦除、4KB扇区擦除。SFUD底层调用擦除时,是按FAL传下来的偏移和长度来的,偏移和长度可能不是4KB对齐,所以最稳妥的办法是让SFUD自己计算需要擦除哪些扇区,然后逐个扇区调用0x20命令完成擦除。

3.2 初始化与芯片探测

SFUD的初始化函数在不同版本里不太一样,老版本用sfud_init,新版本用rt_sfud_init,我用的1.1.0版本是rt_sfud_init。这个函数内部会自动调用sfud_port.c里的初始化接口,遍历所有Flash设备并尝试读取芯片ID。

调用完成后,用sfud_get_device_table()拿到Flash设备表,第一个元素就是我们的W25Q64。为了验证移植是否成功,我会做一个自检:读出芯片ID并打印,W25Q64的ID是0xEF4017,如果打印出来的ID和这个对得上,说明底层通信已经通了。

const sfud_flash *flash = sfud_get_device_table()[0]; printf("Flash device: %s, size: %dKB\r\n", flash->name, flash->chip.capacity / 1024); printf("JEDEC ID: 0x%06X\r\n", flash->chip.id);

这里有个非常容易踩的坑:如果你的代码只调用了初始化,但没打印ID,不要以为初始化成功就等于读写没问题。SPI模式、时钟极性、相位只要有一个不对,芯片ID可能会读到一个错误值。我建议无论如何都要先做ID自检,这是SFUD移植的第一道关卡。

提示:W25Q系列默认支持Mode 0(CPOL=0、CPHA=0)和Mode 3(CPOL=1、CPHA=1)。SFUD内部默认按Mode 0初始化,如果你的SPI配置用的是Mode 3,芯片也能正常工作,但为了和SFUD保持一致,建议全部用Mode 0。


4. FAL移植:给Flash规划出合理的分区

4.1 FAL抽象层到底做了什么

FAL的全称是Flash Abstraction Layer,它要解决的核心问题,是让应用层不再关心“数据存在哪个物理地址”,只关心“数据属于哪个分区”。

它内部维护了一张“Flash设备表”和一张“分区表”。Flash设备表描述物理Flash,比如名叫norflash的W25Q64,起始地址0,长度8MB;分区表则在上面划分逻辑空间,比如app分区占前4MB,param分区占中间1MB,log分区占后面2MB。

每次读写时,FAL先根据分区名在分区表里找到对应的物理设备、起始地址和偏移量,然后调用Flash设备表里注册的底层回调函数。这套机制的优势很明显:如果以后把W25Q64换成了W25Q128,物理容量变了,只需要改Flash设备表的长度和分区表,应用层代码一个字节都不用动。

4.2 移植的关键环节与配置

FAL的移植核心在fal_flash_sfud_port.c,这个文件是SFUD和FAL之间的桥梁。我需要实现一个fal_flash_dev结构体,并填上init、read、write、erase四个函数指针。

static int fal_flash_init(void) { return 0; } static int fal_flash_read(long offset, uint8_t *buf, size_t size) { const sfud_flash *flash = sfud_get_device_table()[0]; if (sfud_read(flash, offset, size, buf) != SFUD_SUCCESS) { return -1; } return size; } static int fal_flash_write(long offset, const uint8_t *buf, size_t size) { const sfud_flash *flash = sfud_get_device_table()[0]; if (sfud_write(flash, offset, size, buf) != SFUD_SUCCESS) { return -1; } return size; } static int fal_flash_erase(long offset, size_t size) { const sfud_flash *flash = sfud_get_device_table()[0]; if (sfud_erase(flash, offset, size) != SFUD_SUCCESS) { return -1; } return size; } const struct fal_flash_dev w25q64_flash = { .name = "norflash", .addr = 0, .len = 8 * 1024 * 1024, .blk_size = 4 * 1024, .ops = {fal_flash_init, fal_flash_read, fal_flash_write, fal_flash_erase}, .write_gran = 1, };

blk_size是Flash的物理块大小,W25Q64最小擦除单位是4KB,所以这里填4096。write_gran是写粒度,有写命令能按字节写入的都填1,不能按字节写入的才填其他值。

接下来是分区表配置,在fal_cfg.h文件里定义。我这次划分了四个分区,分别用于App固件、参数、日志和OTA下载:

#define FAL_PART_TABLE \ { \ {FAL_PART_MAGIC_WORD, "app", "norflash", 0, 4 * 1024 * 1024, 0}, \ {FAL_PART_MAGIC_WORD, "param", "norflash", 4 * 1024 * 1024, 1 * 1024 * 1024, 0}, \ {FAL_PART_MAGIC_WORD, "log", "norflash", 5 * 1024 * 1024, 2 * 1024 * 1024, 0}, \ {FAL_PART_MAGIC_WORD, "download", "norflash", 7 * 1024 * 1024, 1 * 1024 * 1024, 0}, \ }

这里最重要的规则就是地址对齐。分区表里的起始地址和长度必须按Flash的扇区大小对齐,W25Q64每个扇区4KB,所以起始地址和长度必须是4096的整数倍。我第一次划错的时候,把param分区的起始地址写成了4MB+1024,结果FAL读出来的数据全是乱的。

初始化顺序是:先sfud_init,再fal_init。fal_init内部会遍历Flash设备表,把SFUD拿到的Flash设备注册进FAL的管理范围,然后用打印日志的方式确认分区表解析成功。看到类似[FAL] Partition table load ok且各分区名称、大小与配置一致,基本就说明FAL这层已经通了。

提示:FAL本身不管理数据可靠性,它只是地址转换器。擦写均衡、掉电保护这些逻辑要放在上面的flashDB层去处理。不要指望FAL帮你实现“写不坏”的能力。


5. flashDB移植:从“读写Flash”进化到“存取数据”

5.1 KV模式还是TSDB模式,到底怎么选

flashDB的两种模式,对应两种截然不同的业务场景。

KV模式适合“设备的配置中心”。比如设备序列号、校准系数、服务器IP、用户参数配置,这些数据的特点是:总量不大、按键访问、更新频繁但不会无限增长。KV模式下,flashDB内部会管理扇区的状态,新值写入时不需要擦除整个扇区,而是在空闲区域追加新记录,旧记录通过标志位标记为废弃,等扇区写满了再做垃圾回收。这样既保证了掉电安全,又减少了擦写次数。

TSDB模式适合“带时间戳的数据流”。比如环境监测设备每小时记录一次温湿度,能耗采集设备每分钟记录一次电量数据。这种场景数据只会持续追加,很少修改,而且查询时通常按时间范围过滤。

区分方法其实很简单:数据是“改来改去”还是“只增不减”。如果是前者选KV,如果是后者选TSDB。如果你的需求两者都有,就分两个分区分别建库。我这次在param分区上建KV库,在log分区上建TSDB库,互不干扰。

5.2 移植步骤和基本用法

flashDB的移植工作主要集中在工程配置和初始化代码。首先要确保fdb_cfg.h里宏定义正确:

#define FDB_USING_KVDB #define FDB_USING_TSDB #define FDB_WRITE_GRAN 1 #define FDB_BIG_ENDIAN 0 #define FDB_SECTOR_SIZE 4096

FDB_SECTOR_SIZE必须和Flash物理扇区大小一致,W25Q64就是4096。这个配置不匹配的话,flashDB在格式化和垃圾回收时会出现灾难性的错误,后面避坑部分会详细说。

KV库初始化的代码非常简洁:

#include <flashdb.h> struct fdb_kvdb kv_db; void kv_db_init(void) { fdb_err_t result = fdb_kvdb_init(&kv_db, "env_db", "param", NULL, NULL); if (result != FDB_NO_ERR) { printf("KVDB init failed: %d\r\n", result); } }

注意fdb_kvdb_init的第三个参数传的就是FAL分区表里的分区名param,flashDB内部会调用FAL接口,拿着这个分区名去定位物理地址。

写入和读取也很直观。存一个整型参数:

struct fdb_blob blob; int sensor_offset = 88; // 写入 fdb_kv_set_blob(&kv_db, "sensor_offset", &sensor_offset, sizeof(sensor_offset)); // 读取 int read_value = 0; size_t len = fdb_kv_get_blob(&kv_db, "sensor_offset", &read_value, sizeof(read_value)); if (len > 0) { printf("sensor_offset = %d\r\n", read_value); }

TSDB库的初始化和使用类似,只是多了一步迭代查询的配置:

struct fdb_tsdb ts_db; void tsdb_init(void) { fdb_err_t result = fdb_tsdb_init(&ts_db, "sensor_db", "log", NULL, NULL); if (result != FDB_NO_ERR) { printf("TSDB init failed: %d\r\n", result); } } void tsdb_append(int temperature, int humidity) { struct fdb_blob blob; struct sensor_data { int temp; int humi; } data = {temperature, humidity}; blob.data = &data; blob.size = sizeof(data); fdb_tsl_append(&ts_db, &blob); }

查询就是遍历某个时间范围内的所有记录,把取出的数据交给回调函数处理:

void query_cb(fdb_tsl_t tsl, void *arg) { struct sensor_data data; fdb_blob_read((fdb_blob_t)(tsl->blob), &data, sizeof(data)); printf("temp=%d, humi=%d\r\n", data.temp, data.humi); } void tsdb_query(void) { fdb_time_t from = 0; fdb_time_t to = 0xFFFFFFFF; fdb_tsl_iter(&ts_db, from, to, query_cb, NULL); }

到这里,整条链路就全通了:应用层调flashDB的API,flashDB通过FAL分区定位物理地址,FAL通过SFUD驱动操作W25Q64。


6. 实际踩过的坑与排查方法

6.1 高频问题速查表

整理一下这次移植过程中遇到的高频问题,直接按表格对照,节省排查时间:

现象可能原因解决思路
SFUD扫描不到芯片,读ID失败SPI引脚、模式配置错误;CS引脚未拉低用逻辑分析仪看时序,确认Mode 0下的波形;检查供电和去耦电容
SFUD读到了错误IDSPI速率过高,信号完整性不足把SPI时钟分频调低,比如从最高速降到18MHz
FAL打印的分区表全部失败fal_cfg.h未正确包含;分区名和设备名不匹配核对FAL_FLASH_DEV_TABLE和FAL_PART_TABLE的定义是否一致
FAL读分区数据全0xFFFAL底层erase或write的回调函数未正确转发到SFUD在fal_flash_write和fal_flash_erase里临时加打印,确认有没有被调用
flashDB初始化失败,错误码为FDB_INIT_FAILEDFDB_SECTOR_SIZE和Flash物理扇区不一致统一改成4096,重新擦除整个Flash后再试
KV写入后立刻读取正常,重启后丢失分区被固定为只读;FAL未正确初始化检查分区表里是否配置了只读标志位,检查初始化顺序
flashDB运行一段时间后所有扇区损坏垃圾回收时断电或配置不对确认FDB_WRITE_GRAN值,W25Q系列写粒度就是1;重新格式化Flash

6.2 我在排障时的几点心得

第一,不要跳步。SFUD、FAL、flashDB三层,每一层都要单独验证通过再往上一层走。最忌讳的是三个一起移植完,然后跑一个demo,挂了之后从三个组件里找问题,排查面太大。

第二,善用擦除命令作为兜底。flashDB第一次使用前,如果之前这片Flash里有过旧数据,很容易初始化失败。最稳妥的办法是写一小段临时代码,通过SFUD直接把整个Flash擦掉,然后再初始化flashDB。注意flashDB初始化后第二次启动时不需要特殊处理,它自己会识别已格式化的扇区。

第三,printf的日志别看它简单,关键时刻能救命。SFUD和FAL都自带调试宏开关,建议在调试阶段把SFUD_DEBUG和FAL_DEBUG打开,里面每一行关键操作都有输出。打开方式是在Keil的C/C++选项卡里定义SFUD_USING_SFUD_DBG和FAL_DEBUG,再配一个重定向到串口的fputc函数。


7. 从参数到OTA:这套方案在真实项目里的应用

7.1 设备参数管理实现

这套三件套方案,第一个实际落地的场景是设备参数管理。设备里有一批出厂校准参数,需要在生产线上通过串口或上位机逐台写入,如果没有KV库,这种需求要么用固定的Flash地址硬存,要么自己写一套搬运逻辑。

用flashDB的KV模式之后,生产流程变得很简单。每个设备刷完固件后,上位机通过私有协议把校准参数发到MCU,MCU调用fdb_kv_set_blob逐条写入。启动时再通过fdb_kv_get_blob读出来,读不到就用默认值。这中间还顺手解决了一个历史痛点:参数结构体从v1升级到v2时,字段数量变了,但设备又不想清空重写,我直接把版本号作为一个key存进去,读取时判断版本号,旧版本参数做一次迁移就行。

7.2 日志采集与链路验证

另一个场景是数据日志。设备每隔5秒采样一次电流和电压,数据量不大但全天都在持续产生,如果存在裸的Flash区域,管理起来非常费劲。TSDB模式解决得很干净,每条记录带时间戳,写入时调用fdb_tsl_append,查询时按时间区间迭代,配合一个简单的导出命令,就能把历史数据通过串口打包发给上位机分析。

链路验证时,我故意做了一个掉电测试:写入一批日志后随机断电重启,再查询所有日志。flashDB在掉电保护方面做得很扎实,没有出现日志丢失或整个文件系统损坏的情况。这一点比很多自己写的落盘方案要放心得多。

7.3 扩展思路:和OTA配合使用

第三件事是给OTA升级预留了download分区。有了FAL的分区表,App固件和升级固件的地址被天然隔离。Bootloader里做固件校验时,只需要把模板裁判定在download分区,下载完校验通过后执行跳转,App里的分区表定义让新旧固件不会互相踩踏。这套架构从单机固件升级,到后续想加更多业务分区,都非常灵活。


最后说点实在的。FAL、SFUD、flashDB这套组合,看起来是三段代码拼接,实际上是一个完整的存储架构方法论。最值钱的不是那几百行移植代码,而是它逼着你在一开始就把存储这件事想清楚:底层怎么解耦、分区怎么划分、数据可靠性交给谁。如果你目前还在裸操作Flash写参数、攒日志,建议花一个下午把这套东西跑通,后面省下的调试时间绝对不止一个下午。再分享一个小技巧:把三件套的配套测试demo一次性固化到工程里,比如加一个“写入一批数据然后重启校验”的自检命令,以后每一次改板子、换芯片,都能在两分钟内确认存储链路是否正常。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 3:38:48

SAP FICO成本中心分割结构配置避坑指南

1. 为什么KA06/KL01前置步骤出错&#xff0c;会让整个成本中心分割结构“瘫痪”&#xff1f;在SAP FICO模块里&#xff0c;成本中心分割结构&#xff08;Cost Center Split Structure&#xff09;不是个孤立配置项——它像一座桥&#xff0c;一端连着主数据&#xff08;成本中心…

作者头像 李华
网站建设 2026/10/3 3:38:30

Python数据分析课程设计报告模板与pandas实战代码解析

简介&#xff1a;这是一份基于Python的数据分析课程设计完整资料包&#xff0c;面向高校数据相关专业学生以及需要课程设计参考模板的初、中级开发者。内容覆盖需求分析、数据源获取与读取、数据预处理、数据清洗与规整、业务指标分析和可视化呈现全流程&#xff0c;目录按章节…

作者头像 李华
网站建设 2026/10/3 3:38:18

从零搭建AI工程体系:分层解耦与工程化实践指南

1. 从零搭建AI工程体系&#xff0c;为什么我劝你别一上来就搞模型"ai-engineering-from-scratch"这个标题&#xff0c;第一次看到的时候我愣了一下。不是因为陌生&#xff0c;恰恰相反&#xff0c;是因为它太像我这几年反复在做的事情——从一台干净的机器、一个空目…

作者头像 李华
网站建设 2026/10/3 3:37:54

SQLite接入MCP服务器:让AI助手用自然语言直接查询本地数据库

最近在折腾本地小项目的时候&#xff0c;遇到一个非常实际的需求&#xff1a;手头攒了一堆SQLite数据库文件&#xff0c;有的是爬虫抓的数据&#xff0c;有的是脚本记录的运行日志&#xff0c;还有的是临时分析用的中间结果。每次想查点什么&#xff0c;要么开DB Browser for S…

作者头像 李华
网站建设 2026/10/3 3:37:35

从零构建AI工程:RAG管线与Agent闭环的实战拆解

很多朋友问过我一个问题&#xff1a;入门AI工程&#xff0c;最有效的路径到底是什么。我的答案一直是同一个——别急着上框架&#xff0c;先亲手把一个极简AI工程从零拼一遍。因为只有自己搭过一遍&#xff0c;你才会真正理解那条RAG链路里每一步为什么存在、Agent工具调用是怎…

作者头像 李华
网站建设 2026/10/3 3:37:25

MySQL系统复习笔记:从索引原理到性能调优实战

1. 复习MySQL&#xff0c;到底在复习什么前两天整理电脑里的学习笔记&#xff0c;翻到去年年初给自己定的目标清单&#xff0c;第一条写着“系统复习MySQL”。当时还画了好几个大箭头&#xff0c;从安装部署指向索引优化&#xff0c;从事务隔离指向锁机制&#xff0c;一副要啃下…

作者头像 李华