8.4 KiB
8.4 KiB
2.3 数据库与存储设计规范
数据库与缓存是系统最核心的底座。代码可以随时重构,但糟糕的数据结构一旦上线并积累了大量数据,重构成本将是毁灭性的。本规范旨在确保数据结构的可读性、扩展性及高并发下的性能稳定。
注:本章节主要以关系型数据库(如 MySQL/PostgreSQL)及 Redis 为基准,其他 NoSQL 存储可参照核心思想灵活应用。
2.3.1 表结构与字段设计规范
0. 框架原生范式顺应原则 (极其重要)
许多现代 Web 框架及其绑定的 ORM(如 Django ORM, Ruby on Rails ActiveRecord, Spring Data JPA 等)拥有自己强烈推崇的数据库设计范式(例如:特定的表名生成规则、默认的审计字段命名 created_at、隐含的关联表主外键逻辑)。
- 规约: 在此重申“避免对抗框架”的原则。如果项目高度依赖此类重型全栈框架,请优先遵守框架的原生数据库约定,以最大化利用框架的内置生态和开发效率;但对于使用 MyBatis, GORM, SQLAlchemy 等轻量级或自定义程度较高的 ORM 项目,则必须严格遵守下述通用的结构与命名规范。
1. 命名规范(通用红线)
- 表名与字段名: 必须全部使用小写字母,单词之间使用下划线 _ 分隔(Snake Case)。严禁使用驼峰命名或拼音。
- 表名要求: 必须是名词,建议增加业务模块前缀。例如:sys_user(系统模块-用户)、ord_order(订单模块-订单)。
- 禁用保留字: 严禁使用数据库保留字(如 desc, order, match, key 等)作为表名或字段名。
2. 字段设计与类型选择
- 主键设计: * 默认推荐使用 BIGINT 配合雪花算法 (Snowflake ID) 或单调递增的分布式 ID 作为主键,有利于分库分表。
- 若为中小型单体项目,允许使用数据库自增 AUTO_INCREMENT。
- 强烈不建议使用无序的 UUID 字符串作为主键(会导致 B+ 树频繁页分裂,严重影响写入性能)。
- 金额类型: 涉及财务、金额的字段,必须使用 DECIMAL(如 DECIMAL(10,2))或将元转为分存储为 BIGINT。绝对禁止使用 FLOAT 或 DOUBLE,会产生精度丢失!
- 布尔与枚举值: 建议使用 TINYINT(1) 或 TINYINT(4)。字段命名建议以 is_ 或 has_ 开头(如 is_deleted,has_attachments),并在注释中明确写清枚举含义(如 0:否, 1:是)。
- 字符串类型: 定长使用 CHAR,变长使用 VARCHAR。尽量避免使用 TEXT 或 BLOB;如必须使用,建议将此类大字段垂直拆分到附属表中,以免拖慢主表的查询效率。
- 字符集: 默认强制使用 utf8mb4,以完美支持 Emoji 表情和生僻字;排序规则推荐使用 utf8mb4_general_ci。
3. 必备的审计字段
所有核心业务表,强烈建议包含以下四个基础审计字段(注:若所用框架有内置约定的字段名如 created_at / updated_at,则以框架约定为准):
- id: 主键。
- create_time: 创建时间(推荐 DATETIME 或 TIMESTAMP,默认值 CURRENT_TIMESTAMP)。
- update_time: 更新时间(默认值 CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)。
- is_deleted: 逻辑删除标记(0正常,1删除)。注意:使用逻辑删除时,需仔细评估与唯一索引(Unique Key)的冲突问题。
2.3.2 索引建立原则与慢查询规避
慢查询是导致数据库 CPU 飙升、系统雪崩的头号元凶。开发人员在编写 SQL 时必须脑中有 B+ 树的结构。
1. 索引建立原则 [推荐规范]
- 控制索引数量: 单表索引数量建议不超过 5 个。索引不是越多越好,过多的索引会严重拖慢 INSERT/UPDATE 的性能。
- 高区分度优先: 尽量在区分度高的列上建立索引(如 user_id,order_no)。不要在性别、状态这类只有少数几个值的列上单独建立索引。
- 拥抱联合索引(最左前缀法则): * 频繁一起查询的多个字段,应建立联合索引,而非多个单列索引。
- 联合索引的设计必须遵循最左前缀法则。例如索引为 (a, b, c),那么 WHERE a=?、WHERE a=? AND b=? 会走索引,但 WHERE b=? AND c=? 绝对不会走该联合索引。
- 覆盖索引优先: 尽量让查询的列直接包含在联合索引中,避免“回表”操作,极大提升查询效率。
2. 慢查询规避红线(必须遵守)
- 禁止 SELECT *: 只查询业务需要的字段。SELECT * 会导致无法使用覆盖索引,并增加网络传输负担。
- 禁止左模糊查询: LIKE '%keyword' 或 LIKE '%keyword%' 会导致索引全表扫描。如需全文检索,请引入 ElasticSearch。右模糊 LIKE 'keyword%' 是允许的(可走索引范围查询)。
- 禁止在索引列上做计算或函数操作: 例如 WHERE YEAR(create_time) = 2023 会导致索引失效。应改写为 WHERE create_time >= '2023-01-01' AND create_time < '2024-01-01'。
- 隐式类型转换会导致索引失效: 如果字段类型是 VARCHAR,但查询时写成了 WHERE phone = 13800138000(数字类型),MySQL 会隐式转换导致全表扫描。必须加引号:WHERE phone = '13800138000'。
- 警惕深分页问题: LIMIT 100000, 20 会让数据库扫描 100020 行数据然后丢弃前十万行。
- 解决建议: 限制最大允许跳页数,或者结合 2.2 章节推荐的“游标分页(Cursor)”机制(如 WHERE id > last_max_id LIMIT 20)。
2.3.3 Redis 缓存设计规范
Redis 虽然快,但不当的使用会导致内存泄漏和严重的并发故障。
1. 键值 (Key) 命名规范
为了防止不同业务线的 Key 产生冲突,且便于在可视化工具(如 RDM)中分类查看,Key 必须遵循层级命名法则:
- 格式: 项目名:模块名:实体名:唯一标识
- 示例: * 用户信息缓存:cps:user:info:10086
- 订单防重放锁:cps:order:lock:REQ_99823
- 规范: 全部小写或大写(团队统一即可,推荐小写),严禁包含空格、换行或特殊字符,长度尽量控制在 64 字节以内以节省内存。
2. Value 设计规范
- 控制大 Key(BigKey): 单个 String 类型的 Value 不应超过 10KB;Hash、List、Set 元素个数不应超过 5000 个。大 Key 在删除或传输时会阻塞 Redis 单线程模型。
- 选择合适的数据结构: * 简单的全量缓存用 String (配合 JSON 序列化)。
- 如果只需要频繁更新或读取对象的某几个字段,应使用 Hash,避免频繁的完整 JSON 序列化和反序列化开销。
3. 缓存生命周期与高可用防范(极其重要)
- 强制设置过期时间 (TTL): 绝不允许向 Redis 写入永久不过期的业务缓存(配置类或基准字典除外)。否则 Redis 内存迟早被撑爆。
- 防范缓存雪崩 (Cache Avalanche): * 问题: 大量缓存在同一时刻集体过期,导致巨量请求瞬间全部打向数据库,击穿 DB。
- 对策: 在设置 TTL 时,必须加上一个随机的抖动时间(例如,基础过期时间 2 小时 + 随机 0~300 秒)。
- 防范缓存穿透 (Cache Penetration): * 问题: 黑客恶意查询一个数据库中绝对不存在的值(如 id=-1),导致缓存永远不命中,所有请求全打到 DB。
- 对策: 即使数据库返回 Null,也要把这个 Null 值缓存进 Redis(设置一个较短的 TTL,如 5 分钟);或者引入布隆过滤器 (Bloom Filter) 提前拦截。
- 防范缓存击穿 (Cache Breakdown): * 问题: 某一个“极其热点”的 Key 突然过期的一瞬间,上万个并发请求发现缓存未命中,同时去查询 DB 并尝试重写缓存,导致 DB 崩溃。
- 对策: 使用分布式锁(如 Redisson)控制,只让一个线程去查询 DB 并重建缓存,其他线程等待;或针对极度热点的数据设置“逻辑过期”而非物理过期。
🤖 [附加] AI 助手执行协议 (AI Output Schema)
绝对红线:强制输出两部分内容:1. Mermaid 格式的 ER 图;2. 包含 [表名、字段名、类型、是否为空、默认值、索引说明] 的标准 Markdown 表格。