美文网首页
聊一聊扩展字段设计

聊一聊扩展字段设计

作者: 浅谈架构 | 来源:发表于2020-05-06 11:37 被阅读0次

    1. 背景

    ​ 工作中我们常常有需求需要加字段,如果数据库数据量比较大,新增字段耗时较长,导致性能下降,甚至出现锁表等问题。

    ​ 添加扩展字段, 常见的做法有,

    • 动态添加字段
    • 添加扩展表
    • json方式存储
    • xml方式存储

    ​ 这里我们聊聊基于KV行存储和基于按位存储

    2. 基于KV水平存储

    场景:例如现在有张订单表,需要新增field_1,field_2 字段,并且以后可能会无限扩展字段

    • kv表结构设计
    
    CREATE TABLE `order_kv` (
    
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    
    `order_id` varchar(30) NOT NULL COMMENT '订单编号',
    
    `key` varchar(30) NOT NULL COMMENT '存储字段名',
    
    `value` varchar(3000) NOT NULL DEFAULT '' COMMENT '存储value',
    
    `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
    
    `type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '字段类型: 1: string , 2: json',
    
    PRIMARY KEY (`id`),
    
    UNIQUE KEY `order_id` (`order_id`,`key`),
    
    KEY `idx_create_time` (`create_time`)
    
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单扩展字段KV表';
    
    

    注 :实际场景中会对kv表进行分表/分库, 因为都是id维度,可以按照id 取hash 分表/分库

    • 代码改造如下
    
    //1. 从配置中心获取参数key(这里我就直接写死了)
    
    String fieldKeys = "field_a,field_b";
    
    List fieldKeyArr = Splitter.on(",").trimResults().omitEmptyStrings().splitToList(fieldKeys);
    
    //2. 从request中获取需要存储param
    
    if (CollectionUtils.isEmpty(fieldKeyArr)) {
    
    return;
    
    }
    
    //3. 获取request中的参数
    
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    
    for (String key : fieldKeyArr) {
    
    String value = request.getParameter(key);
    
    if (StringUtils.isEmpty(value)) {
    
    continue;
    
    }
    
    //4组装扩展字段
    
    }
    
    //5 存储扩展字段
    
    
    • 打通查询接口

    查询语句如下:

    
    SELECT  key,value FROM order_KV where order_id = {order_id} and key in('field_a','field_b')
    
    

    查询接口我们可以新增返回字段Map(String,String) extData,将扩展字段以map的方式返回,以后新增字段就无需改代码了,只需更改配置中心配置即可。

    3. 基于按位存储设计

    适合场景: 我们经常会有新增字段,并且字段类型为布尔(只有0和1)的场景

    实现思路: 可以新增一个flag(Long型,64位)字段, 由于二进制位只有0和1,所以我们可以利用这个特性来标识只有0和1场景的字段,最多可以表示64个字段。

    ​ 实现方式如下

    • 首先定义一个flag实体,定义标志key 和位置
    
    @Data
    
    public class FlagBO {
    
    @Attribute(name = "key", required = true)
    
    private String key;
    
    @Attribute(name = "position", required = true)
    
    private Byte position;
    
    public Long getHexLong() {
    
    if (position <= 64) {
    
    return (long) Math.pow(2, position -1);//2的n-1次方 二进制位 01 10 100 1000
    
    }
    
    return 0L;
    
    }
    
    }
    
    
    • 定义字段枚举(这里也可以从配置中心定义)
    
    /**
    
    
    
    
    
    */
    
    public enum OrderFlagEnum {
    
    orderFieldBooleanA("orderFieldBooleanA", (byte) 1),
    
    orderFieldBooleanB("orderFieldBooleanB", (byte) 2);
    
    private String key;
    
    private Byte position;
    
    OrderFlagEnum(String key, Byte position) {
    
    this.key = key;
    
    this.position = position;
    
    }
    
    //获取flag定义列表
    
    public static List getOrderFlagBO() {
    
    List flagBOList = new ArrayList<>();
    
    for (OrderFlagEnum orderFlagEnum : OrderFlagEnum.values()) {
    
    FlagBO flagBO = new FlagBO();
    
    flagBO.setKey(orderFlagEnum.key);
    
    flagBO.setPosition(orderFlagEnum.position);
    
    flagBOList.add(flagBO);
    
    }
    
    return flagBOList;
    
    }
    
    }
    
    
    • 从请求参数中获取字段并且组装成flag
    
    /**
    
    * encode flag
    
    *
    
    * @param request
    
    * @return
    
    */
    
    public Long getFlagFromRequest(HttpServletRequest request) {
    
    long flag = 0;
    
    //获取flag配置
    
    List orderFlags = getOrderFlags();
    
    if (orderFlags != null && !orderFlags.isEmpty()) {
    
    for (FlagBO orderFlag : orderFlags) {
    
    String parameter = request.getParameter(orderFlag.getKey());
    
    if (StringUtils.isNumeric(parameter)) {
    
    if (Integer.valueOf(parameter) == 0) {
    
    continue;
    
    }
    
    flag = flag | orderFlag.getHexLong();
    
    }
    
    }
    
    }
    
    return flag;
    
    }
    
    
    • 查询的时候我们只需要将flag反解出来即可

    • 同样在查询接口可以添加至extData

    
    /**
    
    * decode flag
    
    * @param flag
    
    */
    
    public Map decodeOrderFlag(Long flag) {
    
    Map flagMap = new HashMap<>();
    
    List orderFlags = getOrderFlags();
    
    if (orderFlags != null && !orderFlags.isEmpty()) {
    
    for (FlagBO orderFlag : orderFlags) {
    
    if ((flag & orderFlag.getHexLong()) == orderFlag.getHexLong()) {
    
    flagMap.put(orderFlag.getKey(), 1);
    
    } else {
    
    flagMap.put(orderFlag.getKey(), 0);
    
    }
    
    }
    
    }
    
    return flagMap;
    
    }
    
    
    • 实际运用中我们会遇到这样的场景,比如更新的时候可能同时有添加删除的场景,如何处理?

    • 我们可以将 添加 和 删除 操作分类为addFlag 和 subFlag

    
    /**
    
    * 获取更新标志位
    
    * addFlag 新增
    
    * subFlag 删除
    
    * flagMap 中 value true 为添加 否则为删除
    
    * @param flagMap
    
    * @return
    
    */
    
    public flagUpdateBO getUpdateFlagBOByMap(Map flagMap) {
    
    flagUpdateBO flagUpdateBO = new flagUpdateBO();
    
    Long addFlag = 0L;
    
    Long subFlag = 0L;
    
    Map flagBOMap = getOrderFlagsMap();
    
    if (flagMap != null && !flagMap.isEmpty()) {
    
    for (Map.Entry flagMapEntry : flagMap.entrySet()) {
    
    //存在
    
    FlagBO flagBO = flagBOMap.get(flagMapEntry.getKey());
    
    if (flagBO != null) {
    
    if (flagMapEntry.getValue() != null && flagMapEntry.getValue() > 0) {
    
    //add flag
    
    addFlag = addFlag | flagBO.getHexLong();
    
    } else {
    
    subFlag = subFlag | flagBO.getHexLong();
    
    }
    
    }
    
    }
    
    }
    
    flagUpdateBO.setAddFlag(addFlag);
    
    flagUpdateBO.setSubFlag(subFlag);
    
    return flagUpdateBO;
    
    }
    
    

    最后更新的sql为:

    
    update order_info set flag = (flag | #{addFlag}) & (~ #{subFlag})
    
    

    4. 总结

    flag和kv对比如下

    |类型|适合规则|存储类型|

    | ---- | -------------------- | --------------------- |

    |flag|只有0和1两种值的情况|0和1|

    |kv|可以任意值|可以存储string 和json|

    相信以上方案可以解决工作上大部分添加字段的需求,如果还有其它比较好的方式欢迎与我探讨。

    相关文章

      网友评论

          本文标题:聊一聊扩展字段设计

          本文链接:https://www.haomeiwen.com/subject/gciightx.html