一、多租户系统与逻辑删除的基础认知
在现代企业级应用开发中,多租户系统已成为支撑大规模用户服务的重要架构模式之一。多租户系统的核心在于让多个的用户群体(即租户)共享同一套应用程序和基础设施,同时确保各租户的数据相互隔离、互不干扰,就像每个租户拥有专属的系统环境一样。这种架构不仅能大幅降低企业的 IT 投入成本,还能提升系统的运维效率和资源利用率,因此在各类行业级应用中得到了广泛应用。
而逻辑删除作为数据安全管理的关键手段,与物理删除有着本质区别。物理删除是直接将数据从数据库中彻底移除,一旦操作完成,数据便无法恢复,这在很多场景下存在明显弊端 —— 例如误操作导致的数据丢失、需要追溯历史数据轨迹的业务场景,物理删除都难以满足需求。逻辑删除则不同,它并非真正删除数据,而是通过在数据表中增加一个特定的状态标识字段(通常称为 “删除标识字段”),当需要删除数据时,仅修改该字段的状态值(比如从 “未删除” 状态改为 “已删除” 状态),数据本身依然保留在数据库中。这种方式既实现了业务层面 “删除” 数据的需求,又为数据恢复、数据审计等操作提供了可能,在注重数据安全性和可追溯性的多租户系统中,具有不可替代的作用。
MyBatis-Plus 作为一款在 MyBatis 基础上进行增的持久层框架,为逻辑删除功能提供了便捷、高效的实现方案。它通过内置的逻辑删除插件,能够自动处理数据的查询、插入、更新和删除过程中的状态标识字段操作,开发者无需编写大量重复的 SQL 语句,极大地简化了开发流程,提升了开发效率。将 MyBatis-Plus 的逻辑删除功能应用于多租户系统中,需要结合多租户系统的数据隔离特性进行针对性设计,才能充分发挥两者的优势,同时规避潜在的风险。
二、MyBatis-Plus 逻辑删除在多租户系统中的设计要点
(一)多租户数据隔离模式与逻辑删除的适配
多租户系统常见的数据隔离模式主要有三种:数据库模式、共享数据库 Schema 模式和共享数据库共享 Schema 模式。不同的隔离模式在数据存储结构和访问控制上存在差异,因此在设计逻辑删除功能时,需要进行相应的适配,以确保逻辑删除操作既能满足业务需求,又不破坏数据隔离的安全性。
在数据库模式下,每个租户拥有专属的数据库,各租户的数据库完全,数据隔离级别最高。这种模式下,逻辑删除的设计相对简单。由于数据库本身相互,MyBatis-Plus 的逻辑删除插件可以直接针对每个租户的数据库进行配置,删除标识字段的定义和操作逻辑在各租户数据库中保持一致即可。开发者只需为每个租户的数据源配置对应的逻辑删除规则,例如指定删除标识字段的名称(如 “is_deleted”)、未删除状态值(如 “0”)和已删除状态值(如 “1”),插件就能自动在 SQL 操作中添加逻辑删除相关的条件,确保查询时只获取未删除的数据,删除时仅修改删除标识字段的状态。
共享数据库 Schema 模式下,多个租户共享同一个数据库,但每个租户拥有的 Schema(数据库模式),Schema 之间相互隔离。这种模式下,逻辑删除的设计需要结合 Schema 的隔离特性。在配置 MyBatis-Plus 逻辑删除时,需要确保逻辑删除插件能够识别不同租户的 Schema,并针对不同 Schema 下的表应用相同的逻辑删除规则。通常可以通过在多租户拦截器中集成逻辑删除的配置信息,当进行数据库操作时,多租户拦截器先确定当前租户的 Schema,再将逻辑删除的条件动态添加到 SQL 语句中,保证对不同 Schema 下的数据进行逻辑删除操作时,既能遵循统一的规则,又不会跨 Schema 访问数据,维护数据隔离的安全性。
共享数据库共享 Schema 模式是隔离级别最低的一种模式,多个租户的数据存储在同一个数据库的同一个 Schema 下,通过在数据表中增加 “租户标识字段”(如 “tenant_id”)来区分不同租户的数据。这种模式下,逻辑删除的设计最为复杂,需要同时处理租户标识和删除标识两个关键字段,确保逻辑删除操作不会影响其他租户的数据。在配置 MyBatis-Plus 时,需要将多租户插件与逻辑删除插件进行协同配置。首先,多租户插件会在所有 SQL 操作中自动添加租户标识字段的条件,确保只能访问当前租户的数据;然后,逻辑删除插件在此基础上,进一步添加删除标识字段的条件,确保查询到的是当前租户的未删除数据,删除操作也仅针对当前租户的指定数据修改删除标识状态。这种协同工作机制,需要严格控制插件的执行顺序,通常让多租户插件先执行,确定数据的租户范围后,再由逻辑删除插件处理数据的删除状态,避出现数据越权访问或逻辑删除操作错误的问题。
(二)逻辑删除字段的设计规范
逻辑删除字段是实现逻辑删除功能的核心,其设计是否合理直接影响逻辑删除操作的准确性和系统的性能。在多租户系统中,设计逻辑删除字段时,需要遵循以下规范:
首先,字段名称的统一性。为了便于开发维护和代码复用,在整个多租户系统中,应统一逻辑删除字段的名称。常见的命名方式有 “is_deleted”“delete_flag” 等,建议选择简洁、语义明确的名称,并在项目开发规范中明确规定,确保所有数据表的逻辑删除字段名称保持一致。这样,在配置 MyBatis-Plus 逻辑删除插件时,只需统一指定字段名称,无需为不同表单独配置,减少了配置工作量,也降低了因字段名称不统一导致的错误风险。
其次,字段类型的选择。逻辑删除字段的类型应根据业务需求和数据库性能进行选择。目前,主流的数据库类型中,常用于逻辑删除字段的类型有 tinyint、bit 和 char 等。tinyint 类型占用存储空间小(通常为 1 字节),取值范围适合表示 “0”(未删除)和 “1”(已删除)两种状态,查询和修改效率高,是大多数场景下的首选类型。bit 类型虽然也能表示两种状态,但在不同数据库中的兼容性存在差异,部分数据库工具对 bit 类型的显示和处理不够友好,可能会给开发和运维带来不便。char 类型(如 char (1))虽然也可以使用,但占用的存储空间比 tinyint 大,且在进行比较操作时效率相对较低,仅在特殊业务场景(如需要表示更多状态的情况)下才考虑使用。在多租户系统中,由于数据量通常较大,选择占用存储空间小、操作效率高的 tinyint 类型作为逻辑删除字段,能够有效提升数据库的性能。
最后,字段默认值的设置。为了确保新插入的数据默认处于 “未删除” 状态,需要为逻辑删除字段设置合理的默认值。通常将默认值设置为 “0”(代表未删除),这样在插入数据时,即使开发者没有显式指定逻辑删除字段的值,数据库也会自动为其赋予 “0” 值,避出现数据初始状态错误的问题。同时,在数据表设计时,应将逻辑删除字段设置为非空字段,防止出现 NULL 值。因为 NULL 值在 SQL 查询中需要特殊处理(如使用 IS NULL 或 IS NOT NULL 条件),会增加 SQL 语句的复杂性,还可能导致逻辑删除插件无法正确识别数据的删除状态,从而出现查询错误或删除操作异常的情况。
(三)MyBatis-Plus 逻辑删除的配置与集成
在多租户系统中集成 MyBatis-Plus 的逻辑删除功能,需要进行一系列的配置操作,确保逻辑删除插件能够与多租户系统的架构和业务逻辑相适配。
首先,引入 MyBatis-Plus 的依赖。在项目的构建文件(如 Maven 的 pom.xml 或 Gradle 的 build.gradle)中,添加 MyBatis-Plus 的核心依赖以及与数据库相关的依赖。确保依赖的版本与项目中其他相关组件(如 MyBatis、Spring Boot 等)的版本兼容,避因版本冲突导致的功能异常。
其次,配置逻辑删除插件。在 Spring Boot 项目中,可以通过配置类的方式来初始化逻辑删除插件。在配置类中,创建 LogicDeleteInterceptor 对象,并设置逻辑删除的相关属性,如删除标识字段的名称、未删除状态值和已删除状态值。例如,通过调用 setLogicDeleteFieldName 方法指定字段名称为 “is_deleted”,调用 setLogicNotDeleteValue 方法设置未删除状态值为 “0”,调用 setLogicDeleteValue 方法设置已删除状态值为 “1”。然后,将该拦截器添加到 MyBatis-Plus 的拦截器链中,确保逻辑删除插件能够生效。
同时,由于多租户系统需要进行数据隔离,还需要配置多租户插件(如 TenantLineInnerInterceptor)。在配置多租户插件时,需要指定租户标识字段的名称(如 “tenant_id”),并设置忽略租户过滤的表或 SQL 语句(如系统配置表、公共字典表等不需要租户隔离的数据表)。在配置拦截器链时,需要注意多租户插件和逻辑删除插件的执行顺序。一般情况下,应将多租户插件放在逻辑删除插件之前,这样在执行 SQL 操作时,会先添加租户标识字段的条件,确定数据的租户范围,再添加逻辑删除字段的条件,筛选出未删除的数据,确保数据隔离和逻辑删除功能的协同工作。
另外,对于实体类的配置,需要在表示数据表的实体类中,为逻辑删除字段添加 @TableLogic 注解。该注解用于告诉 MyBatis-Plus 哪个字段是逻辑删除字段,插件会根据注解的信息自动处理相关的 SQL 操作。例如,在实体类中定义 private Integer isDeleted; 字段,并在该字段上添加 @TableLogic 注解,同时可以通过注解的 value 属性和 delval 属性分别指定未删除状态值和已删除状态值(如果与全局配置一致,也可以不单独指定)。这样,当使用 MyBatis-Plus 的 CRUD 接口(如 BaseMapper 的 selectList、deleteById 等方法)进行操作时,插件会自动在 SQL 语句中添加逻辑删除相关的条件,无需开发者手动编写。
在集成过程中,还需要考虑特殊业务场景下的逻辑删除处理。例如,某些业务表可能需要自定义逻辑删除的规则,或者在删除数据时需要同时更新其他关联字段。对于这类情况,可以通过在 Mapper 接口中编写自定义的 SQL 语句,并在 SQL 语句中显式处理逻辑删除字段和租户标识字段的条件。同时,需要确保自定义 SQL 语句遵循多租户数据隔离和逻辑删除的规则,避出现数据安全问题。
三、MyBatis-Plus 逻辑删除在多租户系统中的风险分析与规避策略
(一)数据越权访问风险与规避
在多租户系统中,数据越权访问是最严重的风险之一。如果逻辑删除功能的设计存在漏洞,可能会导致某个租户能够访问到其他租户的已删除数据,或者在进行逻辑删除操作时误修改其他租户的数据,从而破坏数据隔离的安全性。
导致数据越权访问的原因主要有两个方面:一是多租户插件与逻辑删除插件的协同工作机制出现问题,例如插件执行顺序错误,导致先添加逻辑删除条件,再添加租户标识条件,此时如果 SQL 语句中存在表连接或子查询,可能会出现租户标识条件未覆盖所有数据的情况,从而允许访问其他租户的已删除数据;二是自定义 SQL 语句时忽略了租户标识字段或逻辑删除字段的条件,例如在编写复杂的查询或删除 SQL 时,开发者忘记添加 “tenant_id = 当前租户 ID” 或 “is_deleted = 0” 的条件,导致 SQL 操作跨越了租户边界或包含了已删除数据。
为了规避数据越权访问风险,需要采取以下策略:
首先,严格控制插件的执行顺序。在配置 MyBatis-Plus 的拦截器链时,必须确保多租户插件的执行顺序优先于逻辑删除插件。这样,在处理 SQL 语句时,会先根据当前租户的 ID 添加 “tenant_id = ?” 的条件,将数据范围限定在当前租户内,然后再添加 “is_deleted = 0” 的条件,筛选出当前租户的未删除数据。可以通过在配置类中设置拦截器的顺序来实现,例如在 Spring Boot 项目中,使用 InterceptorRegistration 的 order 方法为多租户插件设置较低的顺序值(顺序值越小,执行优先级越高),为逻辑删除插件设置较高的顺序值。
其次,加自定义 SQL 语句的审核与管控。在多租户系统中,应尽量避编写自定义 SQL 语句,优先使用 MyBatis-Plus 提供的 CRUD 接口和条件构造器,因为这些接口和构造器会自动集成多租户和逻辑删除的处理逻辑。如果确实需要编写自定义 SQL 语句,必须在 SQL 语句中显式添加租户标识字段和逻辑删除字段的条件。例如,在查询数据时,SQL 语句中必须包含 “WHERE tenant_id = #{tenantId} AND is_deleted = 0” 的条件;在删除数据时,必须使用 “UPDATE 表名 SET is_deleted = 1 WHERE tenant_id = #{tenantId} AND id = #{id}” 的语句,而不是直接使用 DELETE 语句。同时,项目团队应建立自定义 SQL 语句的审核机制,在代码提交前,由专人审核自定义 SQL 是否符合多租户和逻辑删除的规则,确保没有遗漏关键条件。
最后,利用数据库层面的权限控制进行辅助防护。虽然多租户系统主要通过应用层的插件实现数据隔离,但可以在数据库层面为每个租户的数据库账号设置相应的权限,进一步限制数据访问范围。例如,在数据库模式或共享数据库 Schema 模式下,为每个租户的账号分配只能访问其专属数据库或 Schema 的权限;在共享数据库共享 Schema 模式下,通过数据库的行级安全策略(Row-Level Security),为每个租户的账号自动添加租户标识字段的过滤条件,即使应用层出现漏洞,数据库层面也能起到一定的防护作用。
(二)数据恢复与数据清理风险与规避
逻辑删除虽然保留了数据,便于数据恢复,但也带来了数据恢复和数据清理方面的风险。一方面,如果数据恢复操作不当,可能会导致已删除的数据被错误恢复,或者恢复的数据与现有数据产生冲突;另一方面,随着时间的推移,数据库中会积累大量的已删除数据,如果不及时清理,会占用大量的存储空间,降低数据库的查询和操作性能。
数据恢复风险主要体现在以下几个方面:一是恢复的数据不属于当前租户,由于多租户系统中数据量大且租户众多,如果在数据恢复前没有严格核实数据的租户标识,可能会将其他租户的已删除数据恢复到当前租户的业务数据中,导致数据混乱;二是恢复的数据状态不一致,已删除数据在数据库中可能已经被其他操作修改(如关联表数据的删除或更新),如果仅恢复主表的已删除数据,而没有同步恢复关联表的数据,会导致数据状态不一致,影响业务功能的正常运行;三是恢复操作没有留下审计记录,数据恢复属于敏感操作,如果没有记录恢复操作的执行人、恢复时间、恢复的数据范围等信息,一旦出现问题,难以追溯责任和排查原因。
为了规避数据恢复风险,需要建立规范的数据恢复流程:
首先,数据恢复前的核实与审批。在进行数据恢复操作前,必须由业务人员提交数据恢复申请,申请中应明确恢复的数据范围(如数据的 ID、租户 ID、删除时间等)、恢复原因和恢复后的预期效果。技术人员接到申请后,需要通过数据库查询工具核实待恢复数据的租户标识、数据状态以及关联数据的情况,确保待恢复数据属于当前租户,且关联数据完整。核实无误后,将申请提交给相关负责人审批,审批通过后才能执行恢复操作。
其次,数据恢复的操作规范。执行数据恢复操作时,应使用事务包裹恢复语句,确保恢复操作要么全部成功,要么全部失败,避出现部分数据恢复的情况。例如,在恢复主表数据的同时,同步恢复关联表中对应的已删除数据,所有恢复操作放在同一个事务中执行。如果恢复操作涉及大量数据,应在业务低峰期(如凌晨)执行,并提前做好数据备份,防止恢复过程中出现意外导致数据损坏。恢复完成后,需要由业务人员对恢复的数据进行验证,确认数据的正确性和业务功能的正常运行。
最后,数据恢复的审计记录。在数据恢复操作执行后,需要自动记录审计日志,日志内容应包括操作人、操作时间、恢复的数据范围、恢复前的数据状态、恢复后的数据状态等信息。审计日志应存储在的日志表中,并且不允许修改和删除,以便后续的审计和追溯。
数据清理风险主要体现在清理不及时或清理操作错误。如果长期不清理已删除数据,会导致数据库表的体积不断增大,索引失效,查询语句的执行时间变长,影响系统的整体性能。而如果清理操作错误,可能会误删除未删除的数据,或者删除了其他租户的已删除数据。
为了规避数据清理风险,需要制定合理的数据清理策略:
首先,确定数据清理的时间阈值。根据业务需求和数据的重要性,为不同的数据表设置不同的清理时间阈值。例如,对于日志类、临时数据类的表,可以将清理时间阈值设置为 3 个月,即删除标识字段状态为 “1” 且删除时间超过 3 个月的数据;对于业务核心数据类的表,可以将清理时间阈值设置为 1 年,确保有足够的时间进行数据追溯和恢复。清理时间阈值的设置需要经过业务部门和技术部门的共同确认,避因清理过早导致数据无法恢复。
其次,采用分批清理的方式。在清理大量已删除数据时,不建议一次性执行大规模的删除操作,因为这样会占用大量的数据库资源,导致数据库锁表,影响正常业务的运行。应采用分批清理的方式,将清理操作分成多个小批次执行,每个批次清理固定数量的数据(如每次清理 1000 条),并在批次之间设置合理的时间间隔(如 10 秒),给数据库留出资源释放的时间。同时,在清理操作前,需要对清理语句进行严格测试,确保清理语句仅针对已删除数据,且租户标识字段条件正确,避误删其他租户或未删除的数据。
最后,清理操作的监控与记录。在执行数据清理操作时,需要实时监控数据库的性能指标(如 CPU 使用率、内存占用、IO 读写速度等),如果发现数据库性能异常,应立即暂停清理操作,待性能恢复正常后再继续。清理操作完成后,需要记录清理日志,包括清理的表名、清理的时间范围、清理的数据量、清理操作的执行人等信息,以便后续进行数据追溯和问题排查。
(三)性能损耗风险与规避
在多租户系统中,随着租户数量和数据量的不断增加,逻辑删除功能可能会带来一定的性能损耗。主要体现在以下几个方面:一是查询操作时,需要额外添加逻辑删除字段的条件,导致查询语句的过滤条件增多,索引使用效率降低,尤其是在数据量较大的表中,可能会出现查询耗时增加的情况;二是随着已删除数据的积累,数据表的体积不断增大,即使查询时过滤掉已删除数据,数据库在数据、加索引时也会消耗更多的资源,影响整体查询性能;三是在进行数据统计、报表生成等复杂业务操作时,需要同时处理租户标识和逻辑删除两个字段的过滤,进一步增加了 SQL 语句的复杂性和执行时间。
为了规避性能损耗风险,需要从查询优化、索引设计、数据管理等多个方面采取措施:
首先,优化查询语句与索引设计。针对逻辑删除字段和租户标识字段,设计合理的联合索引。在多租户系统中,大部分查询操作都会同时涉及租户标识和逻辑删除状态的过滤,因此创建 “tenant_id + is_deleted” 的联合索引,能够大幅提升查询语句的执行效率。例如,在查询当前租户的未删除数据时,联合索引可以直接定位到符合条件的数据,避全表。同时,需要避在查询语句中使用复杂的函数或表达式处理逻辑删除字段和租户标识字段,因为这些操作会导致索引失效,增加查询耗时。此外,还可以利用 MyBatis-Plus 提供的条件构造器进行查询优化,减少不必要的查询条件,确保查询语句简洁高效。
其次,定期优化数据库表结构。对于数据量较大且已删除数据占比较高的表,定期进行表碎片整理和索引重建。随着数据的频繁插入、更新和逻辑删除,数据库表会产生大量碎片,导致数据存储不连续,影响数据查询和读取速度。通过执行数据库的碎片整理命令(如 MySQL 的 OPTIMIZE TABLE 命令),可以消除表碎片,优化数据存储结构。同时,定期重建索引可以更新索引统计信息,提升索引的使用效率,避因索引过时导致的查询性能下降。
最后,采用读写分离与缓存机制。对于查询频率高、数据更新频率低的业务场景,可以采用读写分离架构,将查询操作分流到只读副本数据库,减轻主数据库的压力。同时,利用缓存技术(如 Redis)缓存常用的查询结果,例如当前租户的基础配置数据、高频访问的业务数据等,当再次查询这些数据时,直接从缓存中获取,无需访问数据库,大幅减少数据库的查询压力,提升系统响应速度。在使用缓存时,需要注意缓存的更新策略,确保缓存数据与数据库数据的一致性,避出现数据脏读的情况。
(四)审计与合规风险与规避
在多租户系统中,尤其是涉及金融、医疗、政务等对数据合规性要求较高的行业,逻辑删除功能需要满足严格的审计与合规要求。如果审计机制不完善,可能会导致无法追溯数据的删除操作记录,无法满足监管部门的合规检查要求;如果数据存储和处理不符合相关法规(如数据隐私保护法规),可能会面临法律风险。
审计与合规风险主要体现在以下几个方面:一是删除操作记录不完整,无法记录删除操作的执行人、删除时间、删除的数据范围等关键信息,导致无法追溯删除操作的来源和目的;二是已删除数据的存储不符合隐私保护要求,例如已删除的敏感数据(如用户身份证号、银行卡号等)未进行脱敏处理,存在数据泄露的风险;三是无法提供完整的审计报告,在面对监管部门的合规检查时,无法证明数据的删除操作符合相关法规要求。
为了规避审计与合规风险,需要建立完善的审计机制和合规的数据处理流程:
首先,完善删除操作的审计记录。在多租户系统中,所有涉及逻辑删除的操作都必须记录详细的审计日志。审计日志应包括操作类型(如逻辑删除)、操作人(包括用户名、所属租户 ID)、操作时间、操作 IP 、删除的数据标识(如数据 ID、租户 ID)、删除前的数据状态等信息。审计日志应采用不可篡改的存储方式,例如存储在的审计数据库中,并通过数据加密、访问权限控制等手段确保日志的安全性和完整性。同时,审计日志需要保留足够长的时间(根据相关法规要求,通常为数年),以便在合规检查时能够提供完整的历史记录。
其次,敏感数据的脱敏与保护。对于已删除数据中的敏感信息,需要按照相关法规要求进行脱敏处理。例如,对用户的身份证号进行部分遮挡(如显示为 “110101********1234”)、对银行卡号进行加密存储等,确保即使已删除数据被意外访问,敏感信息也不会泄露。同时,需要对已删除数据的访问权限进行严格控制,只有经过授权的审计人员或管理员才能访问已删除的敏感数据,并且访问操作也需要记录在审计日志中,实现全程可追溯。
最后,定期进行合规检查与评估。建立定期的合规检查机制,由专业的合规团队或第三方机构对多租户系统中逻辑删除功能的实现、审计日志的完整性、敏感数据的保护措施等进行检查和评估,确保系统符合相关法规和行业标准的要求。对于检查中发现的问题,需要及时制定整改方案,并跟踪整改情况,确保问题得到彻底解决。同时,根据法规的更新和业务的变化,及时调整逻辑删除的设计和审计机制,确保系统持续满足合规要求。
四、总结与展望
MyBatis-Plus 的逻辑删除功能在多租户系统中具有重要的应用价值,它不仅能够满足业务层面对数据 “删除” 的需求,还能为数据恢复、审计和追溯提供支持,同时结合多租户系统的数据隔离特性,能够有效保障各租户数据的安全性和性。然而,在实际应用过程中,需要充分考虑多租户数据隔离模式与逻辑删除的适配、逻辑删除字段的设计规范、插件的配置与集成等设计要点,同时针对数据越权访问、数据恢复与清理、性能损耗、审计与合规等潜在风险,采取有效的规避策略,才能充分发挥逻辑删除功能的优势,确保多租户系统的稳定、安全运行。
随着企业级应用的不断发展,多租户系统的规模和复杂度将不断提升,对数据管理的要求也将更加严格。未来,在逻辑删除功能的应用中,可以进一步探索智能化的风险防控机制,例如利用人工智能技术对删除操作进行实时监控和风险预警,自动识别异常的删除行为并及时阻止;同时,可以结合云原生技术,实现逻辑删除与数据备份、容灾恢复等功能的深度集成,提升系统的数据安全性和可靠性。此外,还可以加逻辑删除功能与数据治理体系的融合,通过标准化的字段设计、统一的审计规范,实现多租户系统中数据全生命周期的管理,为企业的数字化转型提供更有力的支撑。
总之,MyBatis-Plus 逻辑删除在多租户系统中的设计与应用是一个需要不断优化和完善的过程,只有结合业务实际需求,持续关注风险防控和合规要求,才能构建出安全、高效、可靠的多租户系统,为企业的发展提供坚实的技术保障。