Files
cps-develop-docs/02 - Design Standard/2.3 数据库与存储设计规范.md
2026-03-24 16:13:32 +08:00

88 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# **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\_deletedhas\_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\_idorder\_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 设计规范**
* **控制大 KeyBigKey** 单个 String 类型的 Value 不应超过 10KBHash、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 并重建缓存,其他线程等待;或针对极度热点的数据设置“逻辑过期”而非物理过期。