MySQL重复数据克星:7种高效处理方案全解析

你是不是也曾遇到过这样的烦恼:明明系统测试时一切正常,上线后却频频出现重复数据;大批量导数据时,总有那么几条"不听话"的记录导致整个事务莫名回滚;或者高并发场景下,日志里满是"Duplicate entry"的错误信息,让你焦头烂额?

今天,我就跟大家分享一些 MySQL 重复数据处理方法,带你搞定这些烦人的"重复小妖精"。

1. 重复数据插入问题分析

1.1 问题本质

在 MySQL 中,当我们使用主键或唯一索引来确保数据唯一性时,如果插入重复数据,MySQL 会抛出类似这样的异常:

rust 复制代码
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'xxx' for key 'xxx'

这个异常本质上是数据库告诉我们:"兄 dei,这条数据已经存在了,别再塞了!"

唯一键定义:唯一键可以是主键或唯一索引,二者在触发唯一性约束时行为一致。主键是特殊的唯一索引,区别在于主键不允许 NULL 值且一个表只能有一个主键,而唯一索引则可以有多个且允许 NULL 值。需注意,对于普通唯一索引,MySQL 将多个 NULL 视为不同值,因此可以插入多条 NULL 唯一键的记录;而主键则完全不允许 NULL。选择约束类型时应考虑字段是否允许为 NULL 的业务需求。

1.2 常见场景图

2. 基础解决方案:使用异常捕获

最基础的方案是使用 try-catch 捕获异常,代码如下:

java 复制代码
public void insertUser(User user) {
    try {
        userMapper.insert(user);
        log.info("用户数据插入成功");
    } catch (org.springframework.dao.DuplicateKeyException e) {
        // Spring框架对SQLIntegrityConstraintViolationException的封装
        log.warn("用户数据已存在: {}", user.getUsername());
        // 可以选择忽略或更新现有数据
    } catch (java.sql.SQLIntegrityConstraintViolationException e) {
        // 使用JDBC直接操作时可能遇到的原生异常
        log.warn("用户数据已存在(JDBC原生异常): {}", user.getUsername());
        // 同样可以处理重复数据
    }
}

这种方法的缺点是:每次遇到重复数据都会产生一个异常,异常的创建和捕获会带来额外的性能开销,尤其在批量操作时性能损耗更明显。

3. 改进方案:预检查+条件插入

一个改进思路是先检查数据是否存在,再决定插入或更新:

java 复制代码
public void insertUserWithCheck(User user) {
    User existingUser = userMapper.selectByUsername(user.getUsername());
    if (existingUser == null) {
        userMapper.insert(user);
    } else {
        // 处理重复数据,比如更新或忽略
    }
}

这种方案的核心价值是减少数据库异常抛出,而非保证数据唯一性。在并发环境下存在竞态条件:检查和插入是两个独立操作,中间可能有其他事务插入相同数据。

解决竞态条件的正确方式

  1. 必须结合数据库唯一索引作为兜底保障
  2. 即使发生并发冲突,最终由数据库约束保证数据唯一性
  3. 应用层做好异常捕获处理,保证业务流程正常进行

在高并发场景下,可以考虑使用分布式锁进一步控制并发问题,增加续租机制确保业务完成前锁不会释放:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void insertUserWithLock(User user) {
    // 获取分布式锁(采用Redisson实现自动续租)
    String lockKey = "user_register:" + user.getUsername();
    RLock lock = redissonClient.getLock(lockKey);

    try {
        // 尝试获取锁,设置自动续租(看门狗机制)
        boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
        if (locked) {
            User existingUser = userMapper.selectByUsername(user.getUsername());
            if (existingUser == null) {
                userMapper.insert(user);
            } else {
                // 处理重复数据
            }
        } else {
            throw new BusinessException("操作频繁,请稍后重试");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new BusinessException("操作被中断");
    } finally {
        // 确保锁释放
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

4. 高效解决方案

4.1 INSERT IGNORE 语句

MySQL 提供了 INSERT IGNORE 语句,当遇到重复数据时会自动忽略错误:

java 复制代码
@Insert("INSERT IGNORE INTO user(username, email, password) VALUES(#{username}, #{email}, #{password})")
int insertIgnore(User user);

执行流程如下:

注意:受影响的行数是实际成功插入的行数,被忽略的行不计入受影响的行数。这点在批量操作时尤为重要,返回值只反映实际插入的记录数,而非处理的总记录数。

4.2 ON DUPLICATE KEY UPDATE 语句

如果需要在遇到重复时更新数据,可以使用 ON DUPLICATE KEY UPDATE:

java 复制代码
@Insert("INSERT INTO user(username, email, password, login_count) " +
        "VALUES(#{username}, #{email}, #{password}, #{loginCount}) " +
        "ON DUPLICATE KEY UPDATE " +
        "email = IF(email = VALUES(email), email, VALUES(email)), " + // 仅当值变化时更新,避免无谓更新
        "login_count = login_count + 1")                              // 累加操作必然更新
int insertOrUpdateLoginCount(User user);

这条语句会在遇到重复主键或唯一索引时,执行 UPDATE 操作而不是插入。

注意 :使用IF(字段 = VALUES(字段), 字段, VALUES(字段))可以避免"静默更新"问题------当新值与旧值相同时,MySQL 不会真正执行更新操作,受影响的行数为 0。这种写法确保只有在值真正变化时才更新。

受影响的行数意义:

  • 1: 新插入记录或更新了已有记录(值发生变化)
  • 0: 行被更新但值未变化
  • 2: 合并了多个唯一索引冲突的记录(较少见)

4.3 REPLACE INTO 语句

REPLACE INTO 是另一种处理方式,它会先尝试插入数据,如果出现重复则删除旧记录,再插入新记录:

java 复制代码
@Insert("REPLACE INTO user(id, username, email, password) VALUES(#{id}, #{username}, #{email}, #{password})")
int replaceUser(User user);

执行过程:

flowchart TD A[开始] --> B{尝试插入数据} B -->|没有重复| C[直接插入成功] B -->|存在重复| D[删除旧记录] D --> E[插入新记录] C --> F[结束] E --> F

重要风险提示

  • 如果表存在外键约束,删除旧记录可能触发级联删除,导致关联数据丢失
  • 使用自增主键时,每次 REPLACE 都会生成新的主键值,导致主键值跳跃
  • 大量使用 REPLACE 会导致更频繁的行删除再插入,增加表碎片和锁竞争

适用场景:无外键依赖、无需保留历史版本、完全覆盖旧数据的场景。

5. 批量处理优化

对于批量数据处理,逐条插入效率低下。下面是更安全的批量插入方案(避免 SQL 注入风险):

java 复制代码
@Mapper
public interface UserMapper {

    @Insert("<script>" +
            "INSERT INTO user(username, email, password) VALUES " +
            "<foreach collection='users' item='user' separator=','>" +
            "(#{user.username}, #{user.email}, #{user.password})" +
            "</foreach>" +
            " ON DUPLICATE KEY UPDATE " +
            "email = VALUES(email), " +
            "password = VALUES(password)" +
            "</script>")
    int batchInsertOrUpdate(@Param("users") List<User> users);

    @Insert("<script>" +
            "INSERT IGNORE INTO user(username, email, password) VALUES " +
            "<foreach collection='users' item='user' separator=','>" +
            "(#{user.username}, #{user.email}, #{user.password})" +
            "</foreach>" +
            "</script>")
    int batchInsertIgnore(@Param("users") List<User> users);
}

使用 JdbcTemplate 时也要注意避免 SQL 注入:

java 复制代码
public int batchInsertWithJdbcTemplate(List<User> users) {
    String sql = "INSERT INTO user(username, email, password) VALUES (?, ?, ?) " +
                 "ON DUPLICATE KEY UPDATE email = VALUES(email)";

    return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement ps, int i) throws SQLException {
            User user = users.get(i);
            ps.setString(1, user.getUsername());
            ps.setString(2, user.getEmail());
            ps.setString(3, user.getPassword());
        }

        @Override
        public int getBatchSize() {
            return users.size();
        }
    }).length;
}

6. Spring Boot 整合方案

在 Spring Boot 项目中,我们可以结合 MyBatis 实现更优雅的解决方案:

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 插入用户数据,遇到重复则更新
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean insertOrUpdateUser(User user) {
        return userMapper.insertOrUpdate(user) > 0;
    }

    /**
     * 批量插入用户数据,忽略重复
     */
    @Transactional(rollbackFor = Exception.class)
    public int batchInsertIgnore(List<User> users) {
        if (CollectionUtils.isEmpty(users)) {
            return 0;
        }

        // 大批量数据,分批处理避免事务过大
        int batchSize = 500; // 根据实际数据大小和数据库配置调整
        int totalInserted = 0;

        for (int i = 0; i < users.size(); i += batchSize) {
            List<User> batch = users.subList(i, Math.min(users.size(), i + batchSize));
            totalInserted += userMapper.batchInsertIgnore(batch);
        }

        return totalInserted;
    }
}

// UserMapper接口
public interface UserMapper {

    @Insert("INSERT INTO user(username, email, password) " +
            "VALUES(#{username}, #{email}, #{password}) " +
            "ON DUPLICATE KEY UPDATE " +
            "email = VALUES(email), password = VALUES(password)")
    int insertOrUpdate(User user);

    @Insert("<script>INSERT IGNORE INTO user(username, email, password) VALUES " +
            "<foreach collection='list' item='user' separator=','>" +
            "(#{user.username}, #{user.email}, #{user.password})" +
            "</foreach></script>")
    int batchInsertIgnore(@Param("list") List<User> users);
}

7. 实用异常处理封装

为了使代码更健壮,我们可以封装一个通用的异常处理工具:

java 复制代码
public class MySqlExceptionHelper {

    // MySQL错误码常量
    public static final int ER_DUP_ENTRY = 1062; // 重复键错误码

    /**
     * 执行可能出现重复键的数据库操作
     * @param operation 数据库操作
     * @param duplicateKeyHandler 重复键处理器
     * @return 处理结果
     */
    public static <T> T executeWithDuplicateKeyHandling(
            Supplier<T> operation,
            Function<Exception, T> duplicateKeyHandler) {

        try {
            return operation.get();
        } catch (DataAccessException e) {
            // 提取原始异常
            Throwable cause = e.getCause();
            if (cause instanceof SQLException) {
                SQLException sqlEx = (SQLException) cause;
                // 通过错误码判断,而非不可靠的字符串匹配
                if (sqlEx.getErrorCode() == ER_DUP_ENTRY) {
                    // 调用重复键处理器
                    return duplicateKeyHandler.apply((Exception) cause);
                }
            }
            // 重新抛出其他异常
            throw e;
        }
    }
}

使用示例:

java 复制代码
public boolean insertUserSafely(User user) {
    return MySqlExceptionHelper.executeWithDuplicateKeyHandling(
        // 正常插入逻辑
        () -> {
            int rows = userMapper.insert(user);
            return rows > 0;
        },
        // 重复键处理逻辑
        ex -> {
            log.warn("用户{}已存在,尝试更新", user.getUsername());
            int rows = userMapper.updateByUsername(user);
            return rows > 0;
        }
    );
}

8. 不同方案性能对比

下面是各种方案在不同场景下的性能对比(基于实际测试数据):

性能测试环境:MySQL 8.0, 16G 内存, SSD 存储, 1 万条记录,20%重复率

索引扫描对性能的影响

  • INSERT IGNOREON DUPLICATE KEY UPDATE直接利用唯一索引的 B+树结构快速判断重复,仅需一次索引查找操作
  • 预检查方案则需要额外的索引查询和多次与数据库交互,增加网络延迟和查询成本
  • 当使用唯一索引的前缀索引(如CREATE UNIQUE INDEX idx_name ON user(username(20)))时,判断重复只比较前 N 个字符,需确保前缀长度足够区分业务数据

事务隔离级别的影响 : 在 RR(Repeatable Read)隔离级别下,预检查方案可能读到旧版本数据,而在实际插入时才发现数据已被其他事务插入,导致出现明明检查过却仍触发唯一键异常的问题。而在 RC(Read Committed)隔离级别下,ON DUPLICATE KEY UPDATE使用快照读,可能减少锁等待;而 RR 隔离级别下可能触发间隙锁,增加锁范围,进一步影响并发性能。

9. 方案原理对比

各种方案在锁机制、事务行为上存在显著差异:

比较表:

方案 锁行为 锁范围 事务复杂度 主键变化 并发友好度
INSERT IGNORE 只锁冲突时不操作 最小 简单 不变 最高
ON DUPLICATE KEY UPDATE 锁已有行并更新 中等 中等 不变 中等
REPLACE INTO 锁已有行,删除后再插入 最大 复杂(删除+插入) 自增主键会变化 最低
分布式锁+预检查 全局分布式锁 跨服务 不变 较低

在高并发写入场景,INSERT IGNORE的锁竞争最小,性能最优;而REPLACE INTO可能导致更多的锁等待和死锁风险。

10. 应用场景案例

10.1 用户注册场景

用户注册时,需要确保用户名或邮箱唯一:

java 复制代码
@Service
public class UserRegistrationService {

    @Autowired
    private UserMapper userMapper;

    public RegisterResult register(RegisterRequest request) {
        User user = new User();
        user.setUsername(request.getUsername());
        user.setEmail(request.getEmail());
        user.setPassword(encryptPassword(request.getPassword()));
        user.setCreateTime(new Date());

        try {
            // 使用INSERT IGNORE插入
            int result = userMapper.insertIgnore(user);

            if (result > 0) { // 成功插入新用户
                return RegisterResult.success();
            } else { // 用户名已存在
                // 查询是否是用户名冲突
                User existingUser = userMapper.selectByUsername(user.getUsername());
                if (existingUser != null) {
                    return RegisterResult.usernameExists();
                } else {
                    // 可能是邮箱冲突
                    return RegisterResult.emailExists();
                }
            }
        } catch (Exception e) {
            log.error("注册异常", e);
            return RegisterResult.error("系统异常");
        }
    }
}

10.2 数据导入场景

批量导入用户数据,忽略重复记录:

java 复制代码
@Service
public class DataImportService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private MetricsService metricsService; // 监控服务

    @Transactional(rollbackFor = Exception.class)
    public ImportResult importUsers(List<UserDTO> userDTOs) {
        ImportResult result = new ImportResult();

        // 数据预处理和验证
        List<User> validUsers = userDTOs.stream()
                .filter(this::isValidUserData)
                .map(this::convertToUser)
                .collect(Collectors.toList());

        if (validUsers.isEmpty()) {
            result.setMessage("没有有效数据");
            return result;
        }

        // 分批处理,每批500条
        int batchSize = 500;
        List<List<User>> batches = new ArrayList<>();
        for (int i = 0; i < validUsers.size(); i += batchSize) {
            batches.add(validUsers.subList(i, Math.min(validUsers.size(), i + batchSize)));
        }

        int totalImported = 0;
        int totalDuplicated = 0;
        List<String> errors = new ArrayList<>();

        for (List<User> batch : batches) {
            try {
                int batchCount = batch.size();
                int imported = userMapper.batchInsertIgnore(batch);
                totalImported += imported;
                totalDuplicated += (batchCount - imported);

                // 记录监控指标
                metricsService.recordMetrics(
                    "user_import_success", imported,
                    "user_import_duplicate", batchCount - imported,
                    "user_import_duplicate_ratio", (batchCount - imported) * 100.0 / batchCount
                );

            } catch (Exception e) {
                log.error("导入批次异常", e);
                errors.add("批次导入错误: " + e.getMessage());
            }
        }

        result.setTotalProcessed(validUsers.size());
        result.setSuccessCount(totalImported);
        result.setDuplicateCount(totalDuplicated);
        result.setErrors(errors);

        return result;
    }
}

10.3 分布式 ID 生成器场景

基于数据库序列的分布式 ID 生成方案,确保生成的 ID 全局唯一:

java 复制代码
@Service
public class SequenceGenerator {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 获取指定业务类型的ID序列段
     * @param type 业务类型
     * @param step 步长(一次获取多少个ID)
     * @return 起始ID,应用可在内存中递增使用
     */
    public long getNextIdBatch(String type, int step) {
        // 使用悲观锁确保并发安全
        String selectSql = "SELECT current_id FROM id_generator WHERE type = ? FOR UPDATE";
        Long currentId = jdbcTemplate.queryForObject(selectSql, Long.class, type);

        if (currentId == null) {
            // 首次使用,初始化序列
            String insertSql = "INSERT INTO id_generator(type, current_id, step) VALUES(?, 0, ?)";
            jdbcTemplate.update(insertSql, type, step);
            currentId = 0L;
        }

        // 更新序列值
        String updateSql = "UPDATE id_generator SET current_id = current_id + ? WHERE type = ?";
        jdbcTemplate.update(updateSql, step, type);

        // 返回当前批次的起始ID
        return currentId;
    }
}

// ID生成器表结构
/*
CREATE TABLE id_generator (
    type VARCHAR(50) PRIMARY KEY,
    current_id BIGINT NOT NULL,
    step INT NOT NULL DEFAULT 1000,
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
*/

10.4 金融交易场景的幂等性设计

金融系统中,支付交易必须确保幂等性,避免重复扣款:

java 复制代码
@Service
public class PaymentService {

    @Autowired
    private TransactionMapper transactionMapper;
    @Autowired
    private AccountMapper accountMapper;

    /**
     * 执行支付交易(幂等操作)
     * 通过订单号+交易类型作为唯一键,确保同一笔交易只执行一次
     */
    @Transactional(rollbackFor = Exception.class)
    public PaymentResult pay(String orderNo, String accountId, BigDecimal amount) {
        // 创建交易记录(使用唯一约束确保幂等)
        TransactionDO transaction = new TransactionDO();
        transaction.setOrderNo(orderNo);
        transaction.setType("PAYMENT");
        transaction.setAccountId(accountId);
        transaction.setAmount(amount);
        transaction.setStatus("PROCESSING");
        transaction.setCreateTime(new Date());

        // 尝试插入交易记录,如果已存在则返回0
        int affected = transactionMapper.insertIgnore(transaction);

        if (affected == 0) {
            // 交易已存在,查询状态返回
            TransactionDO existingTx = transactionMapper.selectByOrderNoAndType(orderNo, "PAYMENT");
            return new PaymentResult(existingTx.getStatus(), "交易已处理");
        }

        try {
            // 执行实际扣款逻辑(此部分必须保证原子性)
            boolean success = accountMapper.deductBalance(accountId, amount) > 0;

            // 更新交易状态
            if (success) {
                transactionMapper.updateStatus(orderNo, "PAYMENT", "SUCCESS");
                return new PaymentResult("SUCCESS", "支付成功");
            } else {
                transactionMapper.updateStatus(orderNo, "PAYMENT", "FAILED");
                return new PaymentResult("FAILED", "余额不足");
            }
        } catch (Exception e) {
            // 异常情况更新交易状态
            transactionMapper.updateStatus(orderNo, "PAYMENT", "ERROR");
            throw e; // 向上抛出异常触发事务回滚
        }
    }
}

@Mapper
public interface TransactionMapper {

    @Insert("INSERT IGNORE INTO transactions(order_no, type, account_id, amount, status, create_time) " +
            "VALUES(#{orderNo}, #{type}, #{accountId}, #{amount}, #{status}, #{createTime})")
    int insertIgnore(TransactionDO transaction);

    @Select("SELECT * FROM transactions WHERE order_no = #{orderNo} AND type = #{type}")
    TransactionDO selectByOrderNoAndType(@Param("orderNo") String orderNo, @Param("type") String type);

    @Update("UPDATE transactions SET status = #{status}, update_time = NOW() " +
            "WHERE order_no = #{orderNo} AND type = #{type}")
    int updateStatus(@Param("orderNo") String orderNo, @Param("type") String type, @Param("status") String status);
}

10.5 实时数据同步场景

设备实时数据采集系统,确保只保留每台设备每个时间点的最新数据:

java 复制代码
@Service
public class DeviceMetricsService {

    @Autowired
    private MetricsMapper metricsMapper;

    /**
     * 记录设备实时指标数据
     * 使用device_id+timestamp作为唯一键,确保同一时间点只保留最新数据
     */
    public void recordMetric(String deviceId, Date timestamp, Double value, String metricType) {
        DeviceMetric metric = new DeviceMetric();
        metric.setDeviceId(deviceId);
        metric.setTimestamp(timestamp);
        metric.setValue(value);
        metric.setMetricType(metricType);
        metric.setCreateTime(new Date());

        // 使用REPLACE INTO确保只保留最新值
        metricsMapper.replaceMetric(metric);
    }

    /**
     * 批量记录设备指标(高性能版本)
     */
    public void batchRecordMetrics(List<DeviceMetric> metrics) {
        if (CollectionUtils.isEmpty(metrics)) {
            return;
        }

        // 分批处理,每批200条
        int batchSize = 200;
        for (int i = 0; i < metrics.size(); i += batchSize) {
            List<DeviceMetric> batch = metrics.subList(i, Math.min(metrics.size(), i + batchSize));
            metricsMapper.batchReplaceMetrics(batch);
        }
    }
}

@Mapper
public interface MetricsMapper {

    @Insert("REPLACE INTO device_metrics(device_id, timestamp, metric_type, value, create_time) " +
            "VALUES(#{deviceId}, #{timestamp}, #{metricType}, #{value}, #{createTime})")
    int replaceMetric(DeviceMetric metric);

    @Insert("<script>" +
            "REPLACE INTO device_metrics(device_id, timestamp, metric_type, value, create_time) VALUES " +
            "<foreach collection='metrics' item='metric' separator=','>" +
            "(#{metric.deviceId}, #{metric.timestamp}, #{metric.metricType}, " +
            "#{metric.value}, #{metric.createTime})" +
            "</foreach>" +
            "</script>")
    int batchReplaceMetrics(@Param("metrics") List<DeviceMetric> metrics);
}

11. 跨库分表场景的去重方案

在分库分表架构中,数据被分散到不同的物理表中,单靠数据库唯一索引无法跨库保证唯一性:

实现示例:

java 复制代码
@Service
public class ShardingUserService {

    @Autowired
    private List<UserMapper> shardedMappers; // 不同分片的mapper
    @Autowired
    private ConsistentHash consistentHash; // 一致性哈希服务

    /**
     * 跨分片用户注册,确保用户名全局唯一
     */
    public RegisterResult registerWithSharding(RegisterRequest request) {
        // 1. 先查询全局唯一索引表,确认用户名不存在
        String username = request.getUsername();

        // 使用分布式锁防止并发插入
        String lockKey = "user:register:" + username;
        try (RedisLockWrapper lock = new RedisLockWrapper(redissonClient, lockKey)) {
            if (!lock.tryLock(5, TimeUnit.SECONDS)) {
                return RegisterResult.busy();
            }

            // 2. 检查全局用户名索引
            if (usernameIndexMapper.exists(username)) {
                return RegisterResult.usernameExists();
            }

            // 3. 生成全局唯一用户ID
            String userId = SnowflakeIdGenerator.nextId();

            // 4. 确定分片(使用一致性哈希算法)
            int shardIndex = consistentHash.getShardIndex(username);
            UserMapper targetMapper = shardedMappers.get(shardIndex);

            // 5. 插入用户数据到对应分片
            User user = createUserFromRequest(request, userId);
            targetMapper.insert(user);

            // 6. 插入全局用户名索引(使用INSERT IGNORE防止并发)
            UserNameIndex index = new UserNameIndex(username, userId, shardIndex);
            usernameIndexMapper.insertIgnore(index);

            return RegisterResult.success(userId);
        } catch (Exception e) {
            log.error("分片用户注册异常", e);
            return RegisterResult.error("系统异常");
        }
    }
}

/**
 * 基于虚拟节点的一致性哈希实现
 */
@Component
public class ConsistentHash {

    private final TreeMap<Long, Integer> virtualNodes = new TreeMap<>();
    private final int numberOfReplicas; // 虚拟节点数量
    private final int shardCount; // 实际分片数

    public ConsistentHash(@Value("${sharding.virtual-nodes:160}") int numberOfReplicas,
                          @Value("${sharding.shard-count:4}") int shardCount) {
        this.numberOfReplicas = numberOfReplicas;
        this.shardCount = shardCount;

        // 初始化虚拟节点
        for (int i = 0; i < shardCount; i++) {
            addShard(i);
        }
    }

    private void addShard(int shardIndex) {
        for (int i = 0; i < numberOfReplicas; i++) {
            String nodeKey = shardIndex + "-" + i;
            long hash = hash(nodeKey);
            virtualNodes.put(hash, shardIndex);
        }
    }

    public int getShardIndex(String key) {
        if (virtualNodes.isEmpty()) {
            return 0;
        }

        long hash = hash(key);
        // 找到第一个大于等于hash的节点
        Map.Entry<Long, Integer> entry = virtualNodes.ceilingEntry(hash);

        // 如果没有找到,则取第一个节点
        if (entry == null) {
            entry = virtualNodes.firstEntry();
        }

        return entry.getValue();
    }

    private long hash(String key) {
        // 使用MurmurHash获得更均匀的哈希分布
        return Hashing.murmur3_128().hashString(key, StandardCharsets.UTF_8).asLong();
    }
}

12. 总结

下面表格全面总结了各种 MySQL 重复数据处理方案的特性和适用场景:

方案 优点 缺点 适用场景 锁粒度 事务复杂度 实现复杂度 维护成本
try-catch 异常捕获 实现简单,通用性强 性能较低,异常开销大 单条插入,低频操作 插入行 简单
预检查+条件插入 逻辑清晰 存在并发问题,需要额外查询 单用户操作,并发低场景 查询+插入行 中等
INSERT IGNORE 语法简单,性能最佳 无法获知哪些记录被忽略 只需插入不存在记录场景 仅冲突行 简单
ON DUPLICATE KEY UPDATE 一条语句完成插入或更新 SQL 较长,需要指定更新字段 需要更新已存在记录场景 已有行 中等 中(需维护更新字段)
REPLACE INTO 语法简单,总是保证最新数据 会删除并重建记录,可能引发级联删除 需要完全覆盖已有数据场景 旧行+新行 复杂(删除+插入) 中(需注意外键)
批量插入方案 高性能,减少数据库交互 实现较复杂 大批量数据导入场景 多行 较大
分布式锁+唯一索引 从源头避免重复数据 实现复杂度高 分布式系统,跨库场景 全局分布式锁 高(跨服务) 高(需维护锁服务)
跨库分表去重 支持分库分表架构 实现极其复杂 大规模分布式系统 分片+全局索引 极高 极高 极高(需分片路由逻辑)

通过合理选择和实现这些方案,我们可以有效解决 MySQL 中的重复数据处理问题,提高系统的健壮性和性能。实际项目中,往往需要根据具体场景组合使用不同策略,例如高并发场景下可能同时使用分布式锁、全局唯一 ID 和数据库唯一索引作为多重保障。

这些方案各有优劣,选择时需考虑业务需求、数据量大小、并发级别和系统架构等因素。在大多数场景下,使用INSERT IGNOREON DUPLICATE KEY UPDATE是既简单又高效的解决方案,而在分布式系统中,还需要加入全局唯一 ID 和分布式锁等机制确保数据一致性。

相关推荐
哞哞不熬夜4 分钟前
JavaEE--初识网络
java·网络·java-ee
noravinsc11 分钟前
redis是内存级缓存吗
后端·python·django
等等54316 分钟前
Java EE初阶——wait 和 notify
java·开发语言
betazhou22 分钟前
基于Linux环境实现Oracle goldengate远程抽取MySQL同步数据到MySQL
linux·数据库·mysql·oracle·ogg
API小爬虫37 分钟前
淘宝按图搜索商品(拍立淘)Java 爬虫实战指南
java·爬虫·图搜索算法
lyrhhhhhhhh1 小时前
Spring 框架 JDBC 模板技术详解
java·数据库·spring
亚林瓜子2 小时前
AWS Elastic Beanstalk控制台部署Spring极简工程
java·spring·云计算·aws·eb
noravinsc2 小时前
django中用 InforSuite RDS 替代memcache
后端·python·django
2401_cf2 小时前
如何创建maven项目
java·maven·intellij-idea
喝醉的小喵2 小时前
【mysql】并发 Insert 的死锁问题 第二弹
数据库·后端·mysql·死锁