对于我的一个微服务项目,我决定将代码构建为控制器、服务和存储库。 API 调用路由到控制器,控制器然后调用服务,服务再调用存储库。每一层都通过接口与下一层交互,以保持事物的解耦。
一切都很好,直到我需要缓存。
想象一下,我已经有一个具有
findById(int id)
方法的 CustomerRepository 接口,并且有一个与数据库交互的实现 (DbCustomerRepositoryImpl
)。现在由于某种原因我需要缓存这些信息。因此,我创建了一个与缓存交互的 CustomerRepository 实现 (CacheCustomerRepositoryImpl
),findById(int id)
执行一个简单的 cache.get()
并返回响应。但是我在哪里处理缓存未命中呢?
DbCustomerRepositoryImpl
进行交互(并且可能写入缓存)。但这导致了服务和存储库的耦合。如果最终我决定只使用 DbCustomerRepositoryImpl
,那么我将有不需要的代码来处理服务中的缓存缺失,或者在最坏的情况下,需要在服务中进行一些代码更改。CacheCustomerRepositoryImpl
将首先检查缓存,在缓存未命中的情况下,与 DbCustomerRepositoryImpl
交互并将结果返回给服务。这看起来很好,因为根据我的说法,存储库层应该只获取数据,无论来自哪里,对服务层来说都无关紧要。但这有一个陷阱。如果我需要缓存业务逻辑的响应,我该怎么办?服务层会调用 CacheCustomerRepositoryImpl
然后调用服务层吗?这种在应用程序中添加了我试图避免的循环依赖。我可以想到这两种情况,每种情况都有自己的优点和缺点。但我需要一些帮助来找出哪种情况可能更好,或者是否有更好的方法在干净的架构中进行缓存。
对于大多数应用程序来说,缓存是一个技术细节,在清洁架构中,应该位于接口适配器层。
为了缓存来自存储库的数据,我将应用装饰器模式。您的 CachingRepository 实现与实际存储库相同的接口,将缓存实现和真实存储库作为构造函数依赖项,然后始终首先在读取时检查缓存,并在缓存未命中的情况下回退到真实存储库。它会在写入 API 调用时更新缓存。 另请参阅:https://youtu.be/e_-bf93vx10?si=LxN8jbHKmhu5PgRe
如果您还需要缓存业务逻辑响应,我会在控制器级别添加单独的缓存。在 Asp.Net 中,这可以通过自定义中间件轻松完成。