今天面了三个3年+的Java后端,
问了一个这样的问题:
“数据库逻辑删除后,怎么做唯一性约束?”
结果都答得磕磕巴巴,只能让他们回家等通知了,建议多刷多练一下场景题再来面试吧!
如果是你,会怎么回答这个问题呢?
由于篇幅限制,只能给大家展示小部分内容,
全套面试笔记及答案【点击此处】即可免费获取
1.22 数据库逻辑删除后,怎么做唯一性约束?
1.22.1 面试考察重点
1. 面试官的考察目标
- 业务设计能力:对逻辑删除业务场景的理解
- 数据库技能:对约束机制的深入掌握
- 解决方案:复杂场景下的问题解决能力
- 工程思维:实际落地时的综合考量
2. 关键关注点
- 约束范围:区分已删除和未删除数据
- 性能影响:方案对数据库性能的影响
- 查询复杂度:对业务查询的侵入性
- 数据恢复:逻辑删除恢复时的处理
1.22.2 面试核心知识点详解
不知道大家有没有遇到这么一种业务场景,在业务中有个唯一约束A,当该业务进行逻辑删除后(设置标
记为删除状态),再往唯一约束列插入相同的值时,此时会报Duplicate entry,但在业务上,该值是必
须要插入的。今天我们就来聊聊处理这种业务场景的几种思路
方案一:不采用逻辑删除,直接物理删除
方案二:新建历史表
主表进行物理删除,同时将删除的记录保存到历史表中
方案三:取消表的唯一约束,同时引入redis来保证唯一约束
取消表的唯一约束,在项目中引入redis,通过redis来判重,新增时往redis set记录,删除时,删除
redis记录
方案四:变更删除标记为时间戳
将删除状态不以0,1表示,而是以时间戳为值,然后将删除状态为与之前的唯一约束A重新组成唯一联合
约束index(A、del_flag),删除时变更del_flag的时间戳
方案五:保留删除标记,同时新建一个字段del_unique_key
保留删除状态位,再新增一个字段del_unique_key,该字段默认值为0,字段类型和大小与主键id保持一
致,同时与原先的唯一约束重新组成联合唯一约束index(A,del_unique_key),业务进行逻辑删除,变更
del_unique_key的值为该删除行的主键id
方案的取舍
方案一得从业务的角度上考虑了,如果物理删除,对业务无损,那就无所谓了。方案二等于需要删除的记
录的表都需要有历史表,如果仅仅是用来实现记录删除记录,感觉有点大材小用。方案三引入redis,虽然
也可以解决问题,但是又额外增加复杂度,同时还得保证redis和数据库的一致性。方案四和方案五其实实
现的思路是一样,不过如果已经是在线上跑的业务,还是推荐用第五种方案,毕竟新增字段正常对已有的
业务影响相对较小,如果是第四种方案,直接将标志位修改为时间戳,可能还会涉及改业务。如果是新增
业务,第四种和第五种方案比较推荐
1.223 面试前准备建议和思路
1. 掌握逻辑删除实现方式
- 标志位设计(is_deleted)
- 状态字段设计(status=active/deleted)
- 删除时间戳(deleted_at)
2. 研究唯一约束技术
- 条件唯一索引
- 复合索引
- 触发器维护
- 应用层校验
3. 准备不同数据库方案
数据库 | 支持特性 |
MySQL | 函数索引(8.0+) |
Oracle | 函数索引 |
SQL Server | 过滤索引 |
PostgreSQL | 部分索引 |
1.22.4 📌面试必过的回答思路
1. 标准回答结构
- 问题分析:逻辑删除对唯一约束的破坏
- 解决方案:分级提供多种技术方案
- 实现细节:具体数据库的实现示例
- 优劣对比:各方案的权衡选择
- 最佳实践:生产环境推荐方案
2. 完整回答示例
"针对逻辑删除后的唯一性约束问题,我将提供以下系统化解决方案:
1. 问题本质分析
逻辑删除后,传统唯一索引依然会认为被标记删除的数据存在,导致新插入的"相同"数据违反约束:
-- 原始唯一索引
ALTER TABLE users ADD UNIQUE (username);
-- 逻辑删除后出现问题
UPDATE users SET is_deleted = 1 WHERE id = 1; -- 删除"admin"
INSERT INTO users (username) VALUES ('admin'); -- 被阻止但业务需要允许
2. 解决方案集
方案一:复合唯一索引(兼容性最好)
-- MySQL/PostgreSQL等通用方案
ALTER TABLE users
ADD UNIQUE INDEX uk_username_active (username, is_deleted);
-- 特殊处理:已删除记录is_deleted=id
UPDATE users SET is_deleted = id WHERE id = 1;
优点:全数据库兼容
缺点:需特殊处理删除标记
方案二:过滤索引(现代数据库支持)
-- SQL Server/PG/MySQL8.0+
CREATE UNIQUE INDEX uk_username_active
ON users(username)
WHERE is_deleted = 0;
优点:直观清晰
缺点:数据库版本要求
方案三:删除标记+默认值组合
-- 使用NULL避开唯一约束
ALTER TABLE users
ADD UNIQUE INDEX uk_username (username, deleted_at);
-- 删除时
UPDATE users SET deleted_at = NOW() WHERE id = 1;
优点:利用NULL不参与约束特性
缺点:需改造删除逻辑
方案四:应用层+数据库双校验
// 事务中先查询后插入
@Transactional
public void createUser(String username) {
if (userRepo.existsActiveByName(username)) {
throw new ConflictException("用户名已存在");
}
userRepo.save(new User(username));
}
优点:灵活可控
缺点:存在并发漏洞
3. 生产级推荐方案
分阶段实现:
- 基础防护:复合索引
ALTER TABLE products
ADD UNIQUE INDEX uk_product_code (code, is_deleted);
- 增强防护:应用层校验
@Service
public class ProductService {
@Transactional
public Product createProduct(String code) {
if (productRepository.existsByCodeAndNotDeleted(code)) {
throw new BusinessException("产品编码已存在");
}
return productRepository.save(new Product(code));
}
}