那天早上我们例会,本来以为又是那种平平无奇的周会,结果新来的技术总监一开口就炸场了:“以后谁在生产库用 delete 删数据,直接开除。”全场安静三秒钟,然后一阵低声嘀咕。 其实我第一反应是笑的,这话听起来太夸张了。但他后面举的例子,真让人不敢笑。
线上删库的噩梦
总监说,他以前在一家互联网公司干的时候,有个研发在凌晨修复个脏数据,用了句简单的:
DELETE FROM user_order;
原本想删几条测试订单,结果忘了加 where 条件。数据库没做备份,binlog 又关着,所有订单全没了。那公司当天赔了三百万。 从那以后,他就发了死命令——所有删除操作必须逻辑删除,严禁直接 delete。
什么是逻辑删除?
其实逻辑删除很简单,就是不真正删掉数据,而是给表加个标记字段,比如:
@Column(name = "deleted")
private Boolean deleted = false;
删数据的时候,我们不删,而是把 deleted 改成 true:
@Modifying
@Query("UPDATE User u SET u.deleted = true WHERE u.id = :id")
void softDelete(@Param("id") Long id);
查询时自动带上过滤条件:
@Query("SELECT u FROM User u WHERE u.deleted = false")
List<User> findAllActiveUsers();
这样查出来的就是没被“删”的数据。 逻辑删除的好处是:误删可以恢复、日志可追踪、历史数据还在。缺点就是——表越来越大。
表越来越大怎么办?
我以前也烦这个问题。逻辑删除后几百万条数据,查询会慢得要命。解决办法其实挺多:
加索引给deleted字段加上索引,这样查询活跃数据时过滤更快。
CREATE INDEX idx_deleted ON user(deleted);
分表归档定期把deleted = true的数据转移到历史表,比如:
INSERT INTO user_history SELECT * FROM user WHERE deleted = true;
DELETE FROM user WHERE deleted = true;
视图隔离也可以直接建个视图给业务层用,只暴露活跃数据:
CREATE VIEW active_user AS
SELECT * FROM user WHERE deleted = false;
ORM 层自动化
有时候手写条件太麻烦,Spring JPA 或 MyBatis 其实都能自动处理。 比如用 MyBatis Plus,就一个注解搞定:
@TableLogic(value = "0", delval = "1")
private Integer deleted;
写 delete 语句时,它自动转成 update。 你写:
userMapper.deleteById(1001);
它执行的其实是:
UPDATE user SET deleted = 1 WHERE id = 1001;
不信你可以开个 SQL 日志看看。
为什么 delete 这么危险?
有个很现实的原因:人会犯错。 尤其是凌晨部署、线上热修、写错 where 条件的时候。 比如这条:
DELETE FROM orders WHERE user_id IN (SELECT id FROM users WHERE status='test');
结果子查询多了个括号,删了全表。 或者你用 JPA 写个:
userRepository.deleteAll();
结果开发环境没切回来,一键清空生产库。 这些事不止发生过一次——我自己都差点干过。
实际开发中怎么防?
公司现在的规定基本是这样:
数据库层加保护
delete 权限只给 DBA;
必须开 binlog;
表必须有逻辑删除字段;
所有 delete 操作写触发器自动备份:
CREATE TRIGGER before_user_delete
BEFORE DELETE ON user
FOR EACH ROW
INSERT INTO user_backup SELECT * FROM user WHERE id = OLD.id;
代码层做限制自研的数据访问层直接禁止 delete:
if (sql.contains("delete")) {
throw new RuntimeException("禁止执行物理删除操作!");
}
定期清理真正需要清理的老数据,交给专门的任务处理:
@Scheduled(cron = "0 0 2 * * ?")
public void cleanArchivedData() {
jdbcTemplate.update("DELETE FROM user_history WHERE create_time < DATE_SUB(NOW(), INTERVAL 180 DAY)");
}
这样既安全,又能保持数据库干净。
写在最后
总监那天说完“delete 开除”的话,全场没人反驳。 因为大家都知道,删错一次数据,真的能让人失业。 逻辑删除看起来啰嗦,但它救命。
后来我在 review 别人代码时看到delete from,下意识就出冷汗。 真别觉得夸张,这种事一旦发生,不是写个 SQL 能补回来的。