飙血推荐
  • HTML教程
  • MySQL教程
  • JavaScript基础教程
  • php入门教程
  • JavaScript正则表达式运用
  • Excel函数教程
  • UEditor使用文档
  • AngularJS教程
  • ThinkPHP5.0教程

Mybatis-Plus默认主键策略导致自动生成19位长度主键id的坑

时间:2021-12-10  作者:zhujiqian  

原创/朱季谦

某天检查一位离职同事写的代码,发现其对应表虽然设置了AUTO_INCREMENT自增,但页面新增功能生成的数据主键id很诡异,长度达到了19位,且不是从1开始递增的——
image

我检查了一下,发现该表目前自增主键已经变成从1468844351843872770开始递增了——
image

这就很奇怪了,目前该表数据量很少,且主键是设置AUTO_INCREMENT,正常而言,应该自增id仍在1000范围内,但目前已经变成一串长数字。

底层ORM框架用的是Mybatis-Plus,我寻思了一下,这看起来像是在插入数据库旧自动生成的id,导致并非默认使用MySql的自增AUTO_INCREMENT的id。

因此,决定一步步定位,先给Mybatis-Plus打印出sql日志,看下其insert语句是否自动生成了一个id,然后才插入数据库。

按照网上的教程,我在yaml文件里对应的mybatis-plus配置处设置了开启sql打印日志——

mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  configuration:
    log-impl: 域名域名域名utImpl

然而,很诡异的是,执行操作时并没有打印出sql日志,故而,某一瞬间,我忽然觉得,这群家伙可能都是互相抄的,没有验证当springboot集成了logback时,这样设置并没有效果。

最后,我额外在yaml加了以下配置,才能正常打印MP的sql日志信息——

logging:
  level:
    com:
      zhu:
        test:
          mapper: debug   

接下来,验证一番后,发现,Mybatis-Plus在做insert操作时,确实自动生成了一条长19的数字当做该条数据的id插入到MySql,导致虽然MySql表设置了自增,但被该1468844351843872769影响了,导致下一条数据自动递增值变成了1468844351843872770,这种过长的id值,在做索引维护时,很影响效率,故而,这个问题必须得解决。
image

到这里,就确定,这个长数字的id,是在代码层次就自动生成了,最后进入对应的实体类中,发现该映射数据表的id字段,并没有显示设置对应的主键生成策略。

@Data
@TableName("test")
public class Test extends Model<Test> implements Serializable {
    
    private Long id;
    ......
}

Mybatis-Plus主要有以下几种主键生成策略——

@Getter
public enum IdType {
    /**
     * 数据库ID自增
     */
    AUTO(0),
    /**
     * 该类型为未设置主键类型
     */
    NONE(1),
    /**
     * 用户输入ID
     * 该类型可以通过自己注册自动填充插件进行填充
     */
    INPUT(2),

    /* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
    /**
     * 全局唯一ID (idWorker),根据雪花算法生成19位数字,long类型
     */
    ID_WORKER(3),
    /**
     * 全局唯一ID (UUID)
     */
    UUID(4),
    /**
     * 字符串全局唯一ID (idWorker 的字符串表示),根据雪花算法生成19位字符串,String
     */
    ID_WORKER_STR(5);

    private int key;

    IdType(int key) {
        域名 = key;
    }
}

这里验证了一下,当设置成这样时,就能正常生成数据库自增的id了,使用数据库AUTO_INCREMENT从1开始自增的效果了,当然,其实使用域名也是可以的——

@Data
@TableName("test")
public class Test extends Model<Test> implements Serializable {
    @TableId(value = "id", type = 域名T)
    private Long id;
    ......
}

百度网上的说法,当Mybatis-Plus实体类没有显示设置主键策略时,将默认使用雪花算法生成,也就是域名ORKER或者域名ORKER_STR,具体是long类型的19位还是字符串的19位,应该是根据字段定义类型来判断。

snowflake算法是Twitter开源的分布式ID生成算法,结果是一个long类型的ID 。其核心思想:使用41bit作为毫秒数,10bit作为机器的ID(5bit数据中心,5bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每个毫秒可以产生4096个ID),最后还有一个符号位,永远是0。

接下来,先验证Mybatis-Plus默认主键策略是如何的。

Mybatis-Plus项目在启动时,会对注解实体类进行初始化,然后缓存到系统Map中。

这里,只需要关注Mybatis-Plus源码TableInfoHelper类中的initTableInfo方法即可,这个方法在项目启动时会被调用,然后初始化所有注解@TableName的实体类。与主键根据哪种策略来设置的逻辑在方法initTableFields(clazz, globalConfig, tableInfo)当中——

public synchronized static TableInfo initTableInfo(MapperBuilderAssistant builderAssistant, Class<?> clazz) {
    TableInfo tableInfo = 域名(域名ame());
    if (tableInfo != null) {
        if (域名onfigMark() == null && builderAssistant != null) {
            域名onfigMark(域名onfiguration());
        }
        return tableInfo;
    }

    /* 没有获取到缓存信息,则初始化 */
    tableInfo = new TableInfo();
    GlobalConfig globalConfig;
    if (null != builderAssistant) {
        域名urrentNamespace(域名urrentNamespace());
        域名onfigMark(域名onfiguration());
        域名nderCamel(域名onfiguration().isMapUnderscoreToCamelCase());
        globalConfig = 域名lobalConfig(域名onfiguration());
    } else {
        // 兼容测试场景
        globalConfig = 域名ults();
    }

    /* 初始化表名相关 */
    initTableName(clazz, globalConfig, tableInfo);

    /* 初始化字段相关 */
    initTableFields(clazz, globalConfig, tableInfo);

    /* 放入缓存 */
    域名(域名ame(), tableInfo);

    /* 缓存 Lambda 映射关系 */
    域名teCache(clazz, tableInfo);
    return tableInfo;
}

在初始化字段相关的initTableFields方法里,会判断是否有@TableId 注解,如果没有,就执行initTableIdWithoutAnnotation方法,连续前文提到的,如果实体类id没有加@TableId(value = "id", type = 域名T),那么就会取默认的主键策略。这里的判断是否有@TableId 注解,就是判断是否需要取默认的主键策略,至于具体是如何设置默认主键的,我们可以直接进入到initTableIdWithoutAnnotation方法当中。

public static void initTableFields(Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo) {
    /* 数据库全局配置 */
    域名nfig dbConfig = 域名bConfig();
    List<Field> list = getAllFields(clazz);
    // 标记是否读取到主键
    boolean isReadPK = false;
    // 是否存在 @TableId 注解
    boolean existTableId = isExistTableId(list);

    List<TableFieldInfo> fieldList = new ArrayList<>();
    for (Field field : list) {
        /*
         * 主键ID 初始化
         */
        if (!isReadPK) {
            if (existTableId) {
                isReadPK = initTableIdWithAnnotation(dbConfig, tableInfo, field, clazz);
            } else {
                isReadPK = initTableIdWithoutAnnotation(dbConfig, tableInfo, field, clazz);
            }
            if (isReadPK) {
                continue;
            }
        }
       ......
    }
   ......
}

initTableIdWithoutAnnotation方法——

private static final String DEFAULT_ID_NAME = "id";
/**
 * <p>
 * 主键属性初始化
 * </p>
 *
 * @param tableInfo 表信息
 * @param field     字段
 * @param clazz     实体类
 * @return true 继续下一个属性判断,返回 continue;
 */
private static boolean initTableIdWithoutAnnotation(域名nfig dbConfig, TableInfo tableInfo,
                                                 Field field, Class<?> clazz) {
    //获取实体类字段名
    String column = 域名ame();
    if (域名pitalMode()) {
        column = 域名perCase();
    }
    //当字段名为id
    if (域名lsIgnoreCase(column)) {
        if (域名pty(域名eyColumn())) {
            域名eyRelated(checkRelated(域名derCamel(), 域名ame(), column))
                //设置表策略
                .setIdType(域名dType())
                .setKeyColumn(column)
                .setKeyProperty(域名ame())
                .setClazz(域名eclaringClass());
            return true;
        } else {
            throwExceptionId(clazz);
        }
    }
    return false;
}

Debug到这里,可以看到,如果没有 @TableId 注解显示设置主键策略情况下,默认设置的是 ID_WORKER(3),即会根据雪花算法生成19位数字,long类型。
image

可以进一步发现,这里的 dbConfig是域名nfig实例,进入到DbConfig类,可以看到原来实体类映射的数据库设置在这里,主键类型默认是域名ORKER。

@Data
public static class DbConfig {

    /**
     * 数据库类型
     */
    private DbType dbType = 域名R;
    /**
     * 主键类型(默认 ID_WORKER)
     */
    private IdType idType = 域名ORKER;
    /**
     * 表名前缀
     */
    private String tablePrefix;
    /**
     * 表名、是否使用下划线命名(默认 true:默认数据库表下划线命名)
     */
    private boolean tableUnderline = true;
    /**
     * String 类型字段 LIKE
     */
    private boolean columnLike = false;
    /**
     * 大写命名
     */
    private boolean capitalMode = false;
    /**
     * 表关键词 key 生成器
     */
    private IKeyGenerator keyGenerator;
    /**
     * 逻辑删除全局值(默认 1、表示已删除)
     */
    private String logicDeleteValue = "1";
    /**
     * 逻辑未删除全局值(默认 0、表示未删除)
     */
    private String logicNotDeleteValue = "0";
    /**
     * 字段验证策略
     */
    private FieldStrategy fieldStrategy = 域名NULL;
}

至于如何生成雪花算法id,这里就不一一详细介绍,具体逻辑是在MybatisDefaultParameterHandler类populateKeys方法里,核心代码如下——

protected static Object populateKeys(MetaObjectHandler metaObjectHandler, TableInfo tableInfo,
                                     MappedStatement ms, Object parameterObject, boolean isInsert) {
    if (null == tableInfo) {
        /* 不处理 */
        return parameterObject;
    }
    /* 自定义元对象填充控制器 */
    MetaObject metaObject = 域名onfiguration().newMetaObject(parameterObject);
    // 填充主键
    if (isInsert && !域名pty(域名eyProperty())
        && null != 域名dType() && 域名dType().getKey() >= 3) {
        Object idValue = 域名alue(域名eyProperty());
        /* 自定义 ID */
        if (域名kValNull(idValue)) {
            if (域名dType() == 域名ORKER) {
                域名alue(域名eyProperty(), 域名d());
            } else if (域名dType() == 域名ORKER_STR) {
                域名alue(域名eyProperty(), 域名dStr());
            } else if (域名dType() == 域名) {
                域名alue(域名eyProperty(), 域名2UUID());
            }
        }
    }
   ......
}

前边提到,默认的主键策略是域名ORKER,这里有一个判断域名dType() == 域名ORKER,对代码Debug可以看到,metaObject的setValue(域名eyProperty(), 域名d())代码的作用,是对注解id进行了值填充。

image
填充的值为域名d()返回的1468970800437465089,刚好是19位长度,这就意味着,这里产生的id值,就是我们最后要找的。

域名d()实现本质,正好是基于Snowflake实现64位自增ID算法,而Snowflake,正是引用了雪花算法——

/**
 * <p>
 * 高效GUID产生算法(sequence),基于Snowflake实现64位自增ID算法。 <br>
 * 优化开源项目 http://域名/yu120/sequence
 * </p>
 *
 * @author hubin
 * @since 2016-08-01
 */
public class IdWorker {

    /**
     * 主机和进程的机器码
     */
    private static final Sequence WORKER = new Sequence();

    public static long getId() {
        return 域名Id();
    }

    public static String getIdStr() {
        return 域名eOf(域名Id());
    }

    /**
     * <p>
     * 获取去掉"-" UUID
     * </p>
     */
    public static synchronized String get32UUID() {
        return 域名omUUID().toString().replace(域名, 域名Y);
    }

}

标签:MySql编程
湘ICP备14001474号-3  投诉建议:234161800@qq.com   部分内容来源于网络,如有侵权,请联系删除。