聊一聊扩展字段设计

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|

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341