业务多写冗余表需要注意哪些问题

75 阅读5分钟

在 MySQL InnoDB 中,为了性能而设计冗余字段,并在业务代码的事务中一次性更新多个表,是一种非常常见的设计模式(有时被称为“反范式化”)。虽然这能有效减少联表查询,提升读取性能,但如果处理不当,确实会引发一系列问题。

经过仔细思考,主要问题可以归结为以下几点:

1. 数据一致性风险

这是最核心、最直接的问题。当同一份数据以冗余的形式存储在多个表中时,必须保证在任何并发场景下,这些数据都能被原子性地更新。

  • 问题场景:假设除了更新 a1 和 a2 表,现在还有一个 a3 表也冗余了 a1 的某个字段。如果在业务代码中,某个事务只更新了 a1 和 a2,但忘记了更新 a3,或者在更新 a3 之前事务因为异常而回滚,就会导致 a3 的数据与其他表不一致。
  • 后果:这种不一致是永久性的,除非有后续的修正操作。依赖于这些冗余数据的业务逻辑将会出现错误,且这类问题非常难以排查,因为单个表的数据看起来是“正确”的。

2. 死锁(Deadlock)的可能性增加

当多个并发事务试图以不同的顺序锁定相同的资源(在这里是表中的行)时,死锁的风险会显著增加。

  • 问题场景

    • 事务 1:先锁定 a1 表的 id=1 的行,然后尝试锁定 a2 表的 id=1 的行。
    • 事务 2:先锁定 a2 表的 id=1 的行,然后尝试锁定 a1 表的 id=1 的行。

    如果这两个事务并发执行,可能会出现以下情况:

    1. 事务 1 成功锁定了 a1 的行。
    2. 与此同时,事务 2 成功锁定了 a2 的行。
    3. 接下来,事务 1 尝试锁定 a2 的行,但该行已被事务 2 锁定,因此事务 1 进入等待。
    4. 同时,事务 2 尝试锁定 a1 的行,但该行已被事务 1 锁定,因此事务 2 也进入等待。
  • 后果:两个事务相互等待对方释放锁,形成循环等待,即“死锁”。InnoDB 有自动的死锁检测机制,它会选择一个事务作为“牺牲品”并将其回滚,以打破僵局。虽然数据库能处理这种情况,但会向应用程序抛出错误,导致事务失败、需要重试,并可能影响用户体验和系统吞吐量。

3. 并发性能下降

为了保证数据一致性,更新多个表意味着事务会持有更多、更长时间的锁。

  • 问题场景:一个需要更新三张表的事务,会分别在 a1, a2, a3 中持有行锁。在这个事务提交之前,任何其他试图修改这三张表中被锁定行的事务都必须等待。
  • 后果:如果这些表是系统的热点表,大量的等待会严重降低系统的并发处理能力。事务执行的时间越长(例如,事务中包含复杂的业务逻辑计算),锁被持有的时间就越长,性能瓶颈就越明显。

4. 业务逻辑耦合与维护成本增加

将数据一致性的维护责任从数据库的范式设计转移到了应用层代码中,这增加了系统的复杂性和风险。

  • 问题场景

    • 代码维护:最初,一个用户名的修改可能只需要更新两张表。后来需求变更,又有一个新功能依赖这个用户名,开发人员可能需要在另一个模块中添加对第三张表的更新。如果遗漏了任何一处,就会产生 bug。
    • 新人接手:新加入的开发者可能不了解这些数据冗余的“约定”,在开发新功能时很容易因为只更新了主表而忽略了冗余表,从而引入数据不一致的问题。
  • 后果:系统的维护成本变高,代码变得脆弱,对开发人员的经验和细心程度要求也更高。

如何缓解这些问题?

尽管存在上述风险,但在追求高性能的场景下,这种设计仍然是必要且常见的。关键在于如何通过规范和技术手段来规避风险:

  1. 保证更新顺序:在应用层代码中,建立一个严格的规则,要求所有需要同时更新多张表的操作,都必须按照固定的顺序来获取锁(例如,按表名的字母顺序)。这样可以有效避免死锁的发生。
  2. 保持事务简短:事务的执行时间应该尽可能短。避免在事务中执行耗时的操作,如复杂的计算、第三方 API 调用等。应该先准备好所有需要更新的数据,然后在一个简短的事务中一次性完成所有数据库写入操作。
  3. 封装与抽象:将更新冗余数据的逻辑封装在一个高内聚的模块或服务中(例如,一个 UserService 的 updateName 方法)。所有需要修改这些数据的代码都必须调用这个封装好的方法,而不是直接操作数据库。这能有效避免遗漏更新,降低维护成本。
  4. 建立数据核对与修复机制:可以定期(例如在凌晨)运行批处理任务,检查冗余数据的一致性。如果发现不一致,进行记录报警并尝试自动修复。这可以作为最后一道防线。

总结

在事务中一次性写入多个表以维护冗余字段,是一种用数据一致性的风险和系统复杂度的提升来换取读取性能的典型权衡(Trade-off)。这种做法本身没有绝对的对错,但在采用它时,必须清醒地认识到其带来的潜在问题,并配套建立相应的编码规范、架构设计和运维机制来管理这些风险。