本文目录导读:

- 核心原则
- 方案一:数据库唯一约束(最可靠,推荐首选)
- 方案二:应用层先查后插(存在并发漏洞,需配合锁)
- 方案三:数据库乐观锁(适用于更新操作的唯一性校验)
- 方案四:基于唯一索引的插入忽略/替换(数据库原生能力)
- 方案五:雪花算法等分布式ID(防止ID重复,而非业务字段重复)
- 最佳实践参考
在Java中校验数据唯一性是一个很常见的需求,通常涉及数据库层面、应用层面以及并发控制三个维度的配合。
以下是几种核心的实现方案和校验逻辑:
核心原则
永远不要只依赖应用层的检查,必须以数据库层为最终防线。 这是因为在高并发场景下,应用层的检查和插入之间存在着“时间差”,可能造成数据重复。
数据库唯一约束(最可靠,推荐首选)
这是最强有力的保障,直接在数据库表字段上添加 UNIQUE 约束。
SQL示例:
-- 单字段唯一 ALTER TABLE user ADD CONSTRAINT uk_username UNIQUE (username); -- 多字段联合唯一(如:同一用户不能有重复的身份证号,或同一用户在同一日期不能有重复的订单) ALTER TABLE user ADD CONSTRAINT uk_user_id_card UNIQUE (user_id, id_card_number);
Java代码中的处理:
当插入/更新违反唯一约束时,数据库会抛出 DuplicateKeyException 或 DataIntegrityViolationException(Spring框架中)。
// 使用Spring Data JPA或MyBatis-Plus
try {
userRepository.save(user);
} catch (DataIntegrityViolationException e) {
// 解析异常原因,判断是否是唯一约束冲突
log.error("数据违反唯一约束:{}", e.getMessage());
throw new BusinessException("用户名已存在,请重新输入");
}
优点: 原子性,无并发问题,性能最高。 缺点: 错误信息不够友好,需要手动解析异常。
应用层先查后插(存在并发漏洞,需配合锁)
这是最简单的思路,但不加锁时不能用于高并发场景。
基础实现(有风险):
// 不推荐:线程A和B可能同时查到不存在,然后同时插入,导致重复
public void addUser(String username) {
User existUser = userMapper.selectByUsername(username);
if (existUser != null) {
throw new BusinessException("用户名已存在");
}
userMapper.insert(new User(username));
}
使用锁解决并发(低并发场景可用):
-
本地锁(Synchronized/ReentrantLock): 仅对单机有效。
// 适用于单体应用,不适用分布式 public synchronized void addUser(String username) { // 先查后插 } -
分布式锁(Redis/Redisson/Zookeeper): 适用于微服务集群。
// 使用Redisson分布式锁 public void addUser(String username) { RLock lock = redissonClient.getLock("lock:user:username:" + username); try { // 尝试加锁,等待5秒,锁有效期10秒 if (lock.tryLock(5, 10, TimeUnit.SECONDS)) { User existUser = userMapper.selectByUsername(username); if (existUser != null) { throw new BusinessException("用户名已存在"); } userMapper.insert(new User(username)); } } finally { lock.unlock(); } }
缺点: 需要额外的中间件(Redis/ZK),且锁颗粒度不好把控。
数据库乐观锁(适用于更新操作的唯一性校验)
适用于需要保证某个业务字段的更新不能重复的场景,通常使用 version 字段或 CAS 思想。
适用场景: 例如在某个活动期间,用户只能对一个订单进行一次“取消操作”,使用 status + version 来保证幂等。
UPDATE order SET status = 'CANCELLED', version = version + 1 WHERE id = 123 AND version = 0 AND status = 'ACTIVE';
update 影响行数为0,说明版本不匹配或状态已变,即“取消”操作被判定为重复或非法。
基于唯一索引的插入忽略/替换(数据库原生能力)
这里不需要应用层做检查,直接依赖数据库的行为。
MySQL/SQLite 示例:
-
INSERT IGNORE:如果唯一键冲突,则静默忽略,不报错(返回影响行数为0)。INSERT IGNORE INTO user(username, email) VALUES ('test', 'test@example.com'); -
INSERT ... ON DUPLICATE KEY UPDATE:如果唯一键冲突,则执行更新操作(用于“有则更新,无则插入”的场景,天然防重复)。INSERT INTO user(username, email) VALUES ('test', 'test@example.com') ON DUPLICATE KEY UPDATE email = VALUES(email);
在MyBatis中的使用:
<insert id="insertOrUpdate" parameterType="User">
INSERT INTO user(username, email)
VALUES(#{username}, #{email})
ON DUPLICATE KEY UPDATE email = #{email}
</insert>
插入后,通过 int result = insertOrUpdate(user); 返回的影响行数来判断,如果返回1是插入,返回2是更新。
雪花算法等分布式ID(防止ID重复,而非业务字段重复)
如果你的唯一性校验是针对主键/流水号,可以使用分布式ID生成方案。
// 使用Twitter Snowflake算法 SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0); Long id = idWorker.nextId(); // 生成全局唯一ID user.setId(id); userMapper.insert(user);
注意: 这只解决了ID唯一的问题,不能解决业务字段(如用户名、身份证号)的唯一性,需要配合方案一或方案四。
最佳实践参考
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 常规数据(用户名、邮箱) | 数据库唯一约束 | 简单、可靠、无并发问题。 |
| 高并发注册/抢购 | 方案一(数据库约束)+ 方案四(INSERT IGNORE / DUPLICATE KEY UPDATE) | 应用层不查,直接插入,根据返回影响行数或捕获异常处理。 |
| 分布式系统 | 方案一(数据库约束)+ 方案二(分布式锁) | 数据库做最终保障,锁在内存/中间件层做提前拦截。 |
| 批量导入(文件) | 方案一(数据库约束)+ 方案四(INSERT IGNORE) | 快速跳过重复行,效率高。 |
| 复杂业务逻辑判断(多字段组合) | 方案一(联合唯一索引)+ 方案二(分布式锁) | 锁住关键业务ID,防止幻读。 |
一句话总结:
永远在数据库添加唯一约束做“硬兜底”,在应用层做“软校验”或“加锁拦截”,不要试图只用应用层的if判空来保证唯一性。