MySQL重复数据处理的七种高效方法

2025-04-25 17:50

本文主要是介绍MySQL重复数据处理的七种高效方法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《MySQL重复数据处理的七种高效方法》你是不是也曾遇到过这样的烦恼:明明系统测试时一切正常,上线后却频频出现重复数据,大批量导数据时,总有那么几条不听话的记录导致整个事务莫名回滚,今天,我就跟大家分...

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

1.1 问题本质

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

Java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'xxx' for key 'xxx'

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

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

1.2 常见场景图

MySQL重复数据处理的七种高效方法

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

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

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. 改进方案:预检查+条件插入

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

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

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

解决竞态条件的正确方式

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

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

@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 语句,当遇到重复数据时会自动忽略错误:

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

执行流程如下:

MySQL重复数据处理的七种高效方法

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

4.2 ON DUPLICATE KEY UPDATE 语句

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

@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 是另一种处理方式,它会先尝试插入数据,如果出现重复则删除旧记录,再插入新记录:

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

执行过程:

MySQL重复数据处理的七种高效方法

重要风险提示

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

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

5. 批量处理优化

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

@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 注入:

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 实现更优雅的解决方案:

@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. 实用异常处理封装

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

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;
        }
    }
}

使用示例:

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重复数据处理的七种高效方法

性能测试环境: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. 方案原理对比

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

MySQL重复数据处理的七种高效方法

比较表:

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

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

10. 应用场景案例

10.1 用户注册场景

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

@Service
public class UserRegistrationService {

    @Autowired
    private UserMapper userMapper;

    public RandroidegisterResult register(RegisterRequest request) {
        User user = new User();
        user.setUsername(request.getUsername());
        usandroider.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 数据导入场景

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

@Service
public class DataImportService {

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

    @Transactional(rollbackFor = Exception.class)
    public ImportResult importUserphps(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 全局唯一:

@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 金融交易场景的幂等性设计

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

@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;

         android   // 更新交易状态
            if (success) {
                transactionMapper.updateStatus(orderNo, "PAYMENTjavascript", "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 实时数据同步场景

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

@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. 跨库分表场景的去重方案

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

实现示例:

@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 和分布式锁等机制确保数据一致性。

以上就是MySQL重复数据处理的七种高效方法的详细内容,更多关于MySQL重复数据处理的资料请关注China编程(www.chinasem.cn)其它相关文章!

这篇关于MySQL重复数据处理的七种高效方法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154364

相关文章

Windows 上如果忘记了 MySQL 密码 重置密码的两种方法

《Windows上如果忘记了MySQL密码重置密码的两种方法》:本文主要介绍Windows上如果忘记了MySQL密码重置密码的两种方法,本文通过两种方法结合实例代码给大家介绍的非常详细,感... 目录方法 1:以跳过权限验证模式启动 mysql 并重置密码方法 2:使用 my.ini 文件的临时配置在 Wi

最详细安装 PostgreSQL方法及常见问题解决

《最详细安装PostgreSQL方法及常见问题解决》:本文主要介绍最详细安装PostgreSQL方法及常见问题解决,介绍了在Windows系统上安装PostgreSQL及Linux系统上安装Po... 目录一、在 Windows 系统上安装 PostgreSQL1. 下载 PostgreSQL 安装包2.

SQL中redo log 刷⼊磁盘的常见方法

《SQL中redolog刷⼊磁盘的常见方法》本文主要介绍了SQL中redolog刷⼊磁盘的常见方法,将redolog刷入磁盘的方法确保了数据的持久性和一致性,下面就来具体介绍一下,感兴趣的可以了解... 目录Redo Log 刷入磁盘的方法Redo Log 刷入磁盘的过程代码示例(伪代码)在数据库系统中,r

mysql中的group by高级用法

《mysql中的groupby高级用法》MySQL中的GROUPBY是数据聚合分析的核心功能,主要用于将结果集按指定列分组,并结合聚合函数进行统计计算,下面给大家介绍mysql中的groupby用法... 目录一、基本语法与核心功能二、基础用法示例1. 单列分组统计2. 多列组合分组3. 与WHERE结合使

Python实现图片分割的多种方法总结

《Python实现图片分割的多种方法总结》图片分割是图像处理中的一个重要任务,它的目标是将图像划分为多个区域或者对象,本文为大家整理了一些常用的分割方法,大家可以根据需求自行选择... 目录1. 基于传统图像处理的分割方法(1) 使用固定阈值分割图片(2) 自适应阈值分割(3) 使用图像边缘检测分割(4)

Java中Switch Case多个条件处理方法举例

《Java中SwitchCase多个条件处理方法举例》Java中switch语句用于根据变量值执行不同代码块,适用于多个条件的处理,:本文主要介绍Java中SwitchCase多个条件处理的相... 目录前言基本语法处理多个条件示例1:合并相同代码的多个case示例2:通过字符串合并多个case进阶用法使用

Mysql用户授权(GRANT)语法及示例解读

《Mysql用户授权(GRANT)语法及示例解读》:本文主要介绍Mysql用户授权(GRANT)语法及示例,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录mysql用户授权(GRANT)语法授予用户权限语法GRANT语句中的<权限类型>的使用WITH GRANT

Mysql如何解决死锁问题

《Mysql如何解决死锁问题》:本文主要介绍Mysql如何解决死锁问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录【一】mysql中锁分类和加锁情况【1】按锁的粒度分类全局锁表级锁行级锁【2】按锁的模式分类【二】加锁方式的影响因素【三】Mysql的死锁情况【1

Python中__init__方法使用的深度解析

《Python中__init__方法使用的深度解析》在Python的面向对象编程(OOP)体系中,__init__方法如同建造房屋时的奠基仪式——它定义了对象诞生时的初始状态,下面我们就来深入了解下_... 目录一、__init__的基因图谱二、初始化过程的魔法时刻继承链中的初始化顺序self参数的奥秘默认

html5的响应式布局的方法示例详解

《html5的响应式布局的方法示例详解》:本文主要介绍了HTML5中使用媒体查询和Flexbox进行响应式布局的方法,简要介绍了CSSGrid布局的基础知识和如何实现自动换行的网格布局,详细内容请阅读本文,希望能对你有所帮助... 一 使用媒体查询响应式布局        使用的参数@media这是常用的