88 lines
8.1 KiB
Markdown
88 lines
8.1 KiB
Markdown
# **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 并重建缓存,其他线程等待;或针对极度热点的数据设置“逻辑过期”而非物理过期。 |