笔者的动态字段扩展解决方案主要针对 Mysql 5.7.8 以下版本, 在 Mysql 5.7.8 已经新增 JSON Data Type https://dev.mysql.com/doc/refman/5.7/en/json.html , 同样适用该方案, 而且情况变得更加简单.
痛点
软件行业唯一不变的就是变化, 比如功能上线之后, 客户或 PM 需要对已有的功能增加一些合理的需求, 完成这些工作必须通过添加字段解决, 或者某些功能的实现需要通过增加字段来降低实现的复杂性等等. 这些问题都会改动线上的数据库表结构, 一旦改动就会导致锁表, 会使所有的写入操作一直等待, 直到表锁关闭, 特别是对于数据量大的热点表, 添加一个字段可能会因为锁表时间过长而导致部分请求超时, 这可能会对企业间接造成经济上的损失.
解决方案
增加 json 格式的扩展字段.
下面配合一些代码来描述这个解决方案, 读者便于去理解.
mysql 数据库脚本:
- DROP TABLE IF EXISTS `cs_dustbin`;
- CREATE TABLE IF NOT EXISTS `cs_dustbin` ( `id` VARCHAR(45) NOT NULL COMMENT '主键自增 id',
- `rfid_no` VARCHAR(20) NOT NULL COMMENT 'rfid 卡号',
- `state` INT(1) NOT NULL COMMENT '垃圾桶状态: 0: 已注销; 1: 未使用; 2: 待使用; 3: 已使用(绑定收集点);',
- `user_id` INT NOT NULL COMMENT '登记人, 负责录入垃圾桶的人',
- `type` INT(1) NOT NULL DEFAULT 1 COMMENT '垃圾桶类型: 1: 餐厨垃圾桶',
- `street_code` INT(11) DEFAULT NULL COMMENT '所在镇街 code, 根据状态, 这里的含义可能是领用镇街, 退还镇街.',
- `create_time` DATETIME NOT NULL DEFAULT now() COMMENT '创建时间',
- `update_time` DATETIME NOT NULL DEFAULT now() COMMENT '更新时间',
- `ext` VARCHAR(1000) NOT NULL DEFAULT '{}' COMMENT '扩展字段',
- ...
- PRIMARY KEY (`id`))
- ENGINE = InnoDB
- COMMENT = '垃圾桶表';
复制代码
Java 代码:
- import com.alibaba.fastjson.JSON;
- import lombok.Data;
- import javax.validation.constraints.NotNull;
- import java.util.Date;
- import java.util.List;
- /**
- * 垃圾桶实体
- * Created by Blink on 6/28/2018 AD.
- *
- * @author Blink
- */
- @Data
- public class Dustbin {
- private String id;
- /**
- * rfid 卡号
- */
- @NotNull
- private String rfidNo;
- /**
- * 垃圾桶状态: 0: 已注销; 1: 未使用; 2: 待使用; 3: 已使用(绑定收集点);
- * 对应 Dustbin.StateEnum 类
- */
- @NotNull
- private Integer state;
- /**
- * 录入垃圾桶的人员 id
- */
- @NotNull
- private Long userId;
- /**
- * 垃圾桶类型: 1: 餐厨垃圾桶
- * DefaultValue: 1
- */
- @NotNull
- private Integer type;
- /**
- * 所在镇街 code
- * 根据状态, 这里的含义可能是领用镇街, 退还镇街
- */
- private Integer streetCode;
- /**
- * 创建时间
- * defaultValue : now()
- */
- @NotNull
- private Date createTime;
- /**
- * 更新时间
- */
- @NotNull
- private Date updateTime;
- /**
- * 扩展字段, 详细数据查看 DustbinExt.java
- * DefaultValue: {}
- */
- private String ext;
- ...
- public DustbinExt getExtObject() {
- return JSON.parseObject(this.getExt(), DustbinExt.class);
- }
- public void setExtObject(DustbinExt ext) {
- this.ext = JSON.toJSONString(ext);
- }
- /**
- * 垃圾桶扩展属性
- * Created by Blink on 6/28/2018 AD.
- *
- * @author Blink
- */
- @Data
- public static class DustbinExt {
- /**
- * 所在镇街
- * 根据状态, 这里的含义可能是领用镇街, 退还镇街, 绑定的镇街
- */
- private String street;
- /**
- * 客户(收集点)id, 绑定收集点的时候需要填入
- * 根据目前的需求(2018-06-29), 当收集点解绑的时候
- * 需要保存垃圾桶最新绑定收集点名称, 所以在解绑垃圾桶的时候不会把这个信息删掉
- * 只有当绑定收集点的时候才把他覆盖
- */
- private Long customerId;
- /**
- * 客户 (收集点) 名称, 绑定收集点的时候需要填入
- * 根据目前的需求(2018-06-29), 当收集点解绑的时候
- * 需要保存垃圾桶最新绑定收集点名称, 所以在解绑垃圾桶的时候不会把这个信息删掉
- * 只有当绑定收集点的时候才把他覆盖
- */
- private String customer;
- /**
- * 损坏部位
- * 1: 桶盖; 2: 桶口; 3: 桶身; 4: 桶轴; 5: 桶底; 6: 桶轮;
- * 对应 DustbinDamagePartEnum 类
- */
- private List<Integer> parts;
- }
- ...
- }
复制代码
mysql 脚本可以看到扩展字段的信息:
ext VARCHAR(1000) NOT NULL DEFAULT '{}' COMMENT '扩展字段'
复制代码
可以看到这么一段 Java 代码:
- ...
- /**
- * 扩展字段, 详细字段查看 DustbinExt 类
- * DefaultValue: {}
- */
- private String ext;
- public DustbinExt getExtObject() {
- return JSON.parseObject(this.getExt(), DustbinExt.class);
- }
- public void setExtObject(DustbinExt ext) {
- this.ext = JSON.toJSONString(ext);
- }
- ...
复制代码
可以看到 ext 字段就是用来存储 json 格式的数据, 它可以动态地增加任何字段, 甚至是对象, 不需要通过 DDL(Data Definition Language) https://baike.baidu.com/item/DDL/21997 去创建字段, 非常适合用来解决上面提到的问题.
Java 代码在这里起到辅助性作用, 通过定义一个内部类 https://baike.baidu.com/item/java内部类 来管理扩展字段的属性, 方便我们了解和管理扩展字段, 提高代码的可读性和可维护性, java 这种方式也是笔者总结出来的较为优雅的做法(个人观点).
局限性
有经验的读者可能会提出, ext 字段在 Mysql 5.7.8 以下版本无法对扩展字段中的某一个或一部分字段建立索引, 因为 Mysql 5.7.8 版本以下不支持(Mysql 5.7.8 支持为 Json Data Type 建立索引 https://dev.mysql.com/doc/refman/5.7/en/create-table-secondary-indexes.html#json-column-indirect-index ).
没错, 这是这个解决方案的一个局限性, 在 Mysql 5.7.8 以下版本, 我的建议是, ext 扩展字段不要存储热点数据, 只存储非热点数据, 这样就可以避免查询操作, 降低维护 ext 字段带来的成本和风险, 那如何识别新增字段是不是热点数据呢? 这个需要结合实际业务需求来判断, 也可以询问对业务和技术更有经验的同事, 便于读者更快得出结论.
终极版解决方案
在一些极端的情况下, 变化可能来得太快, 而我们要的是减少变化带来的成本和风险, 所以在表设计之初可以根据自身经验, 或者找更有经验的人寻求帮助, 预估一下需要预留多少个备用字段, 再配合扩展字段, 基本上可以把改变 (添加字段) 表结构的次数降至一个非常少的次数.
总结
在特殊情况下, 通过扩展字段 + 预留字段基本上可以做到动态扩展字段, 又不会影响为热点数据建立索引的情况, 这样我们得到了一个非常灵活的表结构, 便于我们应对未来的变化, 但是请注意, 要维护好我们的实体, 包括里面的每一个字段, 敬畏每一行代码.
来源: https://juejin.im/post/5b50abe1e51d451964624c39