使用递归唯一性验证的方式生成主键

JadePool为简化JDBC编程提供了主键生成功能,主要通过ProcessVO和Access类实现。ProcessVO的insertKey方法在多例模式下,利用系统时间结合递归唯一性验证来生成主键,确保值的唯一性。用户需要注意每个数据库仅使用DbCenter的一个实例以避免冲突。具体实现细节可查看源代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JadePool作为简化JDBC编程工具,提供主键生成方法是必须的。在JadePool中,ProcessVO用于事务型数据库的DML操作,Access用于非事务型数据库的DML操作,Access参照ProcessVO实现。

目前,JadePool只提供了单主键的键值生成方法,没有提供复合主键的生成方法。这里以ProcessVO为例。ProcessVO提供了三个插入主键的方法,分别是:

public Long insertKey(String tableName);
public Object insertAutoKey(String tableName);
public String insertKey(String tableName, String keyValue);


参数tableName是表的名称,虽然没有提供XML映射、注入映射,但创建ProcessVO对象时,通过实例化DbCenter的某一个实例,数据库的结构信息已经被保存在该实例的相关属性中。在tableName表中,哪一个字段是主键,该字段有哪些属性,这些信息均可以通过访问DbCenter的实例得到。



一、长整形主键键值生成策略
调用insertKey(String tableName)插入长整形键值。

insertKey在多例模式的某一个实例中实现,取值范围是系统时间*10000+[0~9999]。在这个取值范围内,按照从小到大的方向,通过递归唯一性验证的方式获取第一个符合条件的值。用户在使用JadePool时,应当注意,一个数据库只使用DbCenter的一个实例。

一个数据库只使用DbCenter的一个实例,这同使用单例模式的效果是相同的。

插入主键的过程是这样的,ProcessVO的对象向DbCenter的实例请求插入主键键值,DbCenter的实例通过调用自身管理的Table对象的方法makeLongKeyValue()计算唯一性的主键值, 将结果返回给DbCenter的实例,逐步返回给ProcessVO对象。
在Table对象中,makeLongKeyValue()方法调用了一个递归方法onlyLongValue(),用于确保返回的键值是唯一的。


ProcessVO		DbCenter		
   |                   |                   
   |                   |                   
   |                   |                   
insertKey------------->|                   
   |                   |
   |         getTable(tableName).makeLongKeyValue()
   |                   |                   
   |<------------------|                   
   |                   |                   
   |                   |                   

具体的实现,参阅源代码
    /**
     * Make long KeyValue
     *
     * @return System.currentTimeMillis*10000+Num, the Num is between 0 and
     * 9999;for example :2012-11-01 HH:mm:ss as
     * 1351749144390,getting:13517491443897256
     */
    synchronized public long makeLongKeyValue() {
        long t = System.currentTimeMillis();
        return onlyLongValue(t, 0);//only value//递归,直到不等
    }

    private long onlyLongValue(long time, long add) {
        long v;
        long len = 10000;
        long k = time * len + add;
        if (k > lastMakeLongKeyValue) {
            lastMakeLongKeyValue = k;
            v = k;
        } else {
            add++;
            if (add > 9999) {
                v = onlyLongValue(time + 1, 0);
            } else {
                v = onlyLongValue(time, add);
            }
        }
        return v;
    }


有关主键生成策略的话题很多,较有代表性的提法是用系统时间加随机数作为键值,这种方式产生的结果会导致键值不能按照时间的先后排列,如果按照主键键值的大小顺序排队买火车票,这种方式就不合理了。

二、通用键值生成器
调用insertAutoKey(String tableName)可以插入Long、Integer、Short、Double、Float以及String类型的主键键值。除了String类型有一定的条件限制外,其它类型的主键键值均是在原表中最大主键值得基础上加1或更大的数所得。生成过程与长整形主键生成的过程相同,同样经过递归唯一性验证。

具体的实现,参阅源代码

    /**
     * @param field
     * @param fieldValue 从数据库中查询的最大值 生成各种类型的第一个主键值
     */
    synchronized public Object makeObjectKeyValue(Field field, Object fieldValue) throws SQLException {
        String className = field.getTypeClassName();
        Object nextValue = null;
        if (fieldValue != null && !"".equals(fieldValue.toString())) {
            if (className.equals("java.lang.Long")) {
                if (((Long) fieldValue).longValue() == Long.MAX_VALUE) {
                    throw new SQLException("Long value must less than Long.MAX_VALUE " + Long.MAX_VALUE);
                }
                nextValue = new Long(((Long) fieldValue).longValue() + 1);
            }
            if (className.equals("java.lang.Integer")) {
                if (((Integer) fieldValue).intValue() == Integer.MAX_VALUE) {
                    throw new SQLException("Integer value must less than Integer.MAX_VALUE " + Integer.MAX_VALUE);
                }
                nextValue = new Integer(((Integer) fieldValue).intValue() + 1);
            }
            if (className.equals("java.lang.Short")) {
                if (((Short) fieldValue).shortValue() == Short.MAX_VALUE) {
                    throw new SQLException("Short value must less than Short.MAX_VALUE " + Short.MAX_VALUE);
                }
                short v = ((Short) fieldValue).shortValue();
                v++;
                nextValue = new Short(v);
            }
            if (className.equals("java.lang.Float")) {
                if (((Float) fieldValue).floatValue() == Float.MAX_VALUE) {
                    throw new SQLException("Float value must less than Float.MAX_VALUE " + Float.MAX_VALUE);
                }
                Float v = new Float(((Float) fieldValue).floatValue() + 1);
                long l = (long) v.floatValue();
                nextValue = new Float(l);

            }
            if (className.equals("java.lang.Double")) {
                if (((Double) fieldValue).doubleValue() == Double.MAX_VALUE) {
                    throw new SQLException("Double value must less than Double.MAX_VALUE " + Double.MAX_VALUE);
                }
                Double v = new Double(((Double) fieldValue).doubleValue() + 1);
                long l = (long) v.floatValue();
                nextValue = new Double(l);
            }
        } else {
            if (className.equals("java.lang.Long")) {
                nextValue = new Long(0);
            }
            if (className.equals("java.lang.Integer")) {
                nextValue = new Integer(0);
            }
            if (className.equals("java.lang.Short")) {
                short v = 0;
                nextValue = new Short(v);
            }
            if (className.equals("java.lang.Float")) {
                nextValue = new Float(0);
            }
            if (className.equals("java.lang.Double")) {
                nextValue = new Double(0);
            }
        }
        return onlyObjectValue(field, nextValue);//唯一性验证
    }

    private Object onlyObjectValue(Field field, Object keyValue) throws SQLException {
        Object v;
        if (!keyValue.equals(lastMakeObjectKeyValue)) {
            lastMakeObjectKeyValue = keyValue;
            v = keyValue;
        } else {
            v = onlyObjectValue(field, makeObjectKeyValue(field, keyValue));//递归,直到不等
        }
        return v;
    }


三、插入用户指定的主键键值
调用insertKey(String tableName, String keyValue)方法,用来将用户指定的键值keyValue插入到tableName表中。调用Table中的makeStringKeyValue(keyField, keyValue)方法验证主键键值的唯一性、合法性。

具体的实现,参阅源代码

    /**
     * Make String KeyValue 检查用户输入的字段是否存在、其值是否合法和是否唯一
     *
     * @param keyValue 给定键值
     * @return 字段存在,且合法唯一,则返回keyValue本身;否则,返回null
     */
    synchronized public String makeStringKeyValue(String keyField, String keyValue) throws SQLException {
        JadeTool tool = new JadeTool();
        if (keyValue == null) {
            throw new SQLException("the key field value must not be null. ");
        }
        String v = null;
        if (tool.isInFields(fields, keyField) && !keyValue.equals(lastStringKeyValue)) {
            lastStringKeyValue = keyValue;
            v = keyValue;
        }
        if (v == null) {
            throw new SQLException("the key field value already being existence. ");
        }
        return v;//可能返回null值,返回null时,用户应当重新输入新值
    }


<think>嗯,用户之前问过关于MySQL存储过程分批插入时自增ID跳号的问题,现在又提出了更具体的情况。这次他们发现,虽然前100万条数据是完整的,但后续每100万ID范围内实际只有大约95万条数据。我需要仔细分析可能的原因。 首先,用户提到使用存储过程分批插入,每次插入100万条,但实际插入的数据量在后续批次中减少。这可能和自增ID的预分配机制有关。之前解释过,当innodb_autoinc_lock_mode设置为1时,MySQL会预分配自增ID,即使插入的行数没有达到预期,也会导致ID跳跃。但用户现在的情况是每个批次都缺少约5%的数据,这可能仅仅是预分配的问题,还可能涉及到插入过程中的失败或回滚。 接下来,我需要考虑可能的原因。例如,插入过程中是否出现了重复主键冲突,导致部分数据插入失败?或者是否有其他约束(如唯一索引)导致插入被中断?此外,存储过程的设计是否有问题,比如在获取数据源时(如使用information_schema.tables)是否能保证每次都返回足够的行数? 用户提到每次插入100万条数据,但实际只插入了95万多条。这可能意味着每次插入操作实际插入的行数足100万。例如,使用SELECT UUID() FROM information_schema.tables LIMIT 1000000,如果information_schema.tables中的表数量足,可能会导致无法生成足够的行。例如,如果数据库中有1000个表,那么每次只能生成1000行,通过交叉连接或其他方式扩展,但如果配置当,可能导致实际插入的行数足,从而触发自增ID的跳跃。 另外,当实际插入的行数足时,自增ID仍然会按预分配的100万递增,导致后续的ID范围出现空洞。例如,第一次插入100万,ID到100万。第二次预分配100万,但实际插入95万,那么自增ID会跳到200万,但实际只有95万数据,导致101万到200万之间只有95万条数据,剩下的5万ID被跳过。 解决方案方面,需要先确认实际插入的行数是否真的每次都是95万。如果是,那么问题出在插入逻辑上,而是自增机制。需要检查数据源是否足够生成100万行数据。如果数据源足,可能需要使用其他方法来生成足够的测试数据,比如使用递归CTE或连接其他大表。 此外,用户可能在更新数据时根据ID范围来分批次处理,但由于ID存在空洞,导致每个范围内的数据量足。这时候需要调整更新逻辑,依赖连续的ID范围,而是使用更精确的方法,比如记录最大已处理ID,或者使用分页查询。 总结起来,用户的问题可能有两个方面:一是插入时实际行数足,导致ID跳跃;二是更新逻辑依赖连续的ID范围。需要分别针对这两个问题给出解决方案,同时建议用户检查数据生成逻辑和调整更新策略。</think>关于MySQL分批插入后自增ID范围与实际数据量匹配的问题,以下是详细技术解析及解决方案: --- ### 一、现象本质分析 #### 1. **自增ID跳号机制** - **预分配原理**:当`innodb_autoinc_lock_mode=1`时,批量插入会按`预估行数`一次性分配ID范围 - **计算公式**: $$ 新自增值 = 当前自增值 + 每次申请的批量大小 $$ - **示例**: ```sql -- 第1批实际插入1,000,000行 INSERT ... LIMIT 1000000; -- 自增值变为1,000,001 -- 第2批实际插入951,440行(失败或回滚部分数据) INSERT ... LIMIT 1000000; -- 自增值仍会变为2,000,001 ``` #### 2. **数据丢失的典型原因** | 原因类型 | 具体表现 | |-------------------------|--------------------------------------------------------------------------| | **插入源数据足** | 使用`information_schema.tables`等系统表作为数据源时,表数量生成目标行数 | | **唯一约束冲突** | 主键或唯一索引导致部分数据插入失败 | | **事务回滚** | 显式/隐式事务中部分操作失败 | --- ### 二、核心问题定位步骤 #### 1. **验证实际插入行数** ```sql -- 检查每批次的真实插入量 SELECT FLOOR(id / 1000000) AS batch_num, COUNT(*) AS actual_rows FROM your_table GROUP BY batch_num; ``` **预期结果**: ``` +------------+-------------+ | batch_num | actual_rows | +------------+-------------+ | 0 | 1,000,000 | -- 第1批正常 | 1 | 951,440 | -- 后续批次异常 | 2 | 951,440 | +------------+-------------+ ``` #### 2. **排查数据源问题(关键)** ```sql -- 检查数据源的实际生成能力 SELECT COUNT(*) FROM information_schema.tables; -- 典型问题:系统表行数足导致LIMIT失效 ``` **解决方案**: ```sql -- 使用递归CTE生成足够行数(MySQL 8.0+) WITH RECURSIVE cte(n) AS ( SELECT 1 UNION ALL SELECT n+1 FROM cte WHERE n < 1000000 ) INSERT INTO your_table(data) SELECT UUID() FROM cte; ``` --- ### 三、针对性解决方案 #### 方案一:修复数据生成逻辑 ```sql DELIMITER $$ CREATE PROCEDURE reliable_batch_insert() BEGIN DECLARE i INT DEFAULT 0; WHILE i < 10 DO -- 使用可扩展数据源 INSERT INTO your_table(data) SELECT MD5(RAND()) FROM information_schema.columns c1 JOIN information_schema.columns c2; -- 交叉连接获取大基数 SET i = i + 1; END WHILE; END$$ DELIMITER ; ``` **优化点**:通过多表JOIN确保生成足够行数 #### 方案二:精确控制自增ID ```sql -- 关闭自动提交,按实际插入量调整自增值 SET autocommit = 0; START TRANSACTION; INSERT INTO your_table(...) ...; SELECT ROW_COUNT() INTO @actual_rows; ALTER TABLE your_table AUTO_INCREMENT = AUTO_INCREMENT + @actual_rows; COMMIT; ``` **限制**:需关闭自增列的自动管理 #### 方案三:业务层容错设计 ```python # 分页查询替代ID范围查询 last_id = 0 while True: query = f""" SELECT id, data FROM your_table WHERE id > {last_id} ORDER BY id LIMIT 100000 """ batch = execute(query) if not batch: break process(batch) last_id = batch[-1].id ``` **优势**:彻底避免依赖ID连续性 --- ### 四、高级监控方案 #### 1. **自增ID使用率监控** ```sql SELECT TABLE_NAME, AUTO_INCREMENT, TABLE_ROWS, (AUTO_INCREMENT - TABLE_ROWS) AS lost_ids FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'your_db'; ``` #### 2. **死锁与冲突检测** ```sql SHOW ENGINE INNODB STATUS\G -- 关注 LATEST DETECTED DEADLOCK 和 INSERT BUFFER 段落 ``` --- ### 五、终极建议 1. **接受ID连续性**:自增ID设计目标是唯一性,连续性无法保证 2. **采用复合主键**: ```sql CREATE TABLE ( shard_id INT, local_id BIGINT AUTO_INCREMENT, PRIMARY KEY(shard_id, local_id) ); ``` 3. **分布式ID方案**:如Snowflake、UUIDv7等时间有序的全局唯一ID **性能配置调优**: ```sql -- 针对高并发批量插入优化 SET GLOBAL innodb_autoinc_lock_mode = 2; -- 交错模式 ALTER TABLE your_table AUTO_INCREMENT = 1; ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值