35.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--数据缓存
在Web应用中,客户端经常需要从后端服务中获取数据。在很早的Web应用中,后端在收到请求后,会根据请求的参数查询数据库获取数据,然后将数据返回给客户端。这么做的好处是简单易懂,容易实现。但是随着应用的复杂度增加,这种方式会带来一些问题。比如,后端服务需要处理大量的请求,或者需要处理复杂的业务逻辑,每个请求都需要查询数据库,这会导致后端服务的性能下降,响应时间变长。因此,越来越多的Web应用开始使用缓存来提高性能。这篇文章,我们将介绍如何如何使用缓存来优化孢子记账的性能。
一、缓存的概念
在计算机科学中,缓存(Cache)是指存储在计算机内存中的数据副本,用于加速数据的访问。缓存可以存储经常使用的数据,以减少对原始数据源(如数据库)的访问次数,从而提高系统性能。缓存的种类有很多,包括内存缓存、磁盘缓存、分布式缓存等。下面,我们针对这三种缓存类型进行简单介绍。
1.1 内存缓存
内存缓存是应用程序中最常用的缓存方式之一。它通常直接在应用进程的内存空间中维护一份数据副本,能够极大地减少对数据库或其他外部数据源的访问次数。在.NET应用中,可以使用 MemoryCache
或 IMemoryCache
来实现内存缓存。开发者可以将频繁访问的数据(如用户信息、配置项、热点业务数据等)存储到内存缓存中,后续请求直接从缓存读取,避免重复查询数据库。
内存缓存的优点是访问速度极快,实现简单,适合存储体积较小且访问频率高的数据。但它也有一些局限性:缓存数据只在当前应用进程内有效,应用重启或崩溃后缓存会丢失;同时,缓存容量受限于服务器的物理内存,无法存储大量数据。因此,内存缓存更适合单体应用或小型服务场景。
1.2 磁盘缓存
磁盘缓存是将数据存储在磁盘上的一种缓存方式。相比内存缓存,磁盘缓存可以存储更多的数据,但访问速度较慢。在.NET中,可以使用 FileCache
或第三方库(如 CacheManager
)来实现磁盘缓存。磁盘缓存适用于需要持久化存储的数据,如日志文件、用户上传的文件等。
磁盘缓存的优点是数据持久化,重启应用后仍然可以访问缓存数据。此外,磁盘缓存不受物理内存限制,可以存储体积较大的数据,适合保存历史数据、图片、文档等不经常变动但需要长期保存的信息。磁盘缓存还可以作为内存缓存的补充,当内存缓存淘汰数据时,将数据转存到磁盘缓存以备后续访问。
但由于磁盘访问速度较慢,通常不适合存储频繁访问的数据。对于高并发、低延迟的场景,磁盘缓存可能成为性能瓶颈。此外,磁盘缓存需要考虑数据一致性、文件管理和磁盘空间的合理分配,避免因缓存数据过多导致磁盘空间耗尽。因此,在实际应用中,磁盘缓存常与内存缓存结合使用,根据数据访问频率和持久化需求合理选择缓存策略。
1.3 分布式缓存
分布式缓存是指将缓存数据分布在多个服务器上,以提高系统的可扩展性和容错能力。它适用于大规模分布式系统或微服务架构中,能够有效地解决单点故障和性能瓶颈问题。在.NET中,可以使用 Redis
、Memcached
等分布式缓存解决方案。
分布式缓存的优点是可以横向扩展,支持大规模数据存储和高并发访问。它能够将数据分布在多个节点上,提供更高的可用性和性能。分布式缓存还可以实现数据的共享和同步,多个服务实例可以访问同一份缓存数据,避免重复计算和数据冗余。
分布式缓存的实现通常需要使用专门的缓存服务器。开发者可以通过配置连接字符串和缓存客户端库来使用分布式缓存。分布式缓存还支持数据过期、淘汰策略等功能,可以根据业务需求灵活配置。
二、缓存的使用场景
缓存的使用场景非常广泛,主要包括以下几种:
- 热点数据缓存:对于访问频率高、变化不频繁的数据,如用户信息、商品详情等,可以将其存储在缓存中,减少对数据库的访问,提高响应速度。
- 计算结果缓存:对于一些计算开销较大的操作,如复杂查询、统计分析等,可以将计算结果缓存起来,避免重复计算,提高系统性能。
- 会话数据缓存:在Web应用中,用户的会话数据(如登录状态、购物车信息等)可以存储在缓存中,以便快速访问和更新,减少对数据库的压力。
- 配置数据缓存:应用的配置信息(如系统参数、功能开关等)可以存储在缓存中,避免每次都从数据库读取,提高配置读取的效率。
- 临时数据缓存:对于一些临时性的数据,如验证码、临时文件等,可以使用缓存来存储,避免频繁的磁盘IO操作。
- 分布式系统中的共享数据:在微服务架构中,多个服务可能需要访问同一份数据,使用分布式缓存可以实现数据的共享和同步,避免数据不一致的问题。
以上六个场景是缓存使用的常见场景,开发者可以根据具体业务需求选择合适的缓存策略和实现方式。
三、使用缓存的注意事项
使用缓存可以显著提高系统性能,但在实际应用中也需要注意一些问题。首先,缓存并不是万能的,不能完全替代数据库。缓存数据可能会过期、失效或被清除,因此在设计系统时,需要合理规划缓存的使用场景和数据存储策略。其次,缓存的命中率是衡量缓存效果的重要指标,开发者需要通过监控和分析来优化缓存策略,提高命中率。
缓存的使用也需要考虑数据的一致性问题,特别是在分布式系统中,多个服务实例可能同时访问或修改缓存数据。为了保证数据的一致性,可以使用分布式锁、版本号等机制来控制对缓存的访问和修改。此外,缓存的容量管理也是一个重要问题,需要根据业务需求和系统资源合理设置缓存大小,避免缓存溢出或内存泄漏。
缓存的失效策略也需要仔细设计,常见的失效策略包括定时失效、LRU(最近最少使用)淘汰等。定时失效可以设置缓存数据的过期时间,超过时间后自动清除;LRU淘汰则根据数据的访问频率来决定哪些数据应该被清除,以保证缓存中存储的是最常用的数据。
缓存的安全性也是一个重要考虑因素,特别是在存储敏感数据时,需要采取加密、权限控制等措施,防止数据泄露和未授权访问。此外,缓存的监控和日志记录也是必要的,可以帮助开发者及时发现缓存命中率低、访问异常等问题,便于后续优化和故障排查。
我们还要特别注意缓存雪崩、缓存穿透和缓存击穿这三个问题。
缓存雪崩指的是在某个时间点,大量缓存数据同时过期,导致大量请求直接访问数据库,造成数据库压力骤增。为了解决这个问题,可以采用随机过期时间、预热缓存等策略来平衡缓存的过期时间。
缓存穿透是指请求的数据在缓存和数据库中都不存在,导致每次请求都直接访问数据库,造成数据库压力过大。为了解决这个问题,可以使用布隆过滤器等技术来过滤掉不存在的数据请求。
缓存击穿是指某个热点数据在缓存中失效后,所有请求都直接访问数据库,造成数据库压力过大。为了解决这个问题,可以使用互斥锁或信号量来控制对热点数据的访问,确保只有一个请求可以查询数据库并更新缓存。
四、选用什么缓存组件
在.NET中,有多种缓存组件可供选择,常用的包括 MemoryCache
、Redis
、Memcached
等。选择合适的缓存组件需要根据具体的业务需求和系统架构来决定。
- MemoryCache:适用于单体应用或小型服务,简单易用,适合存储体积较小且访问频率高的数据。它直接在应用进程内维护缓存数据,访问速度极快,但数据只在当前应用进程内有效,重启后会丢失。
- Redis:适用于分布式系统或微服务架构,支持大规模数据存储和高并发访问。Redis 是一个开源的内存数据结构存储系统,支持多种数据类型,提供丰富的缓存功能,如数据过期、持久化等。它可以作为分布式缓存解决方案,多个服务实例可以共享同一份缓存数据。
- Memcached:也是一种分布式缓存解决方案,适用于需要高性能缓存的场景。Memcached 是一个高性能的分布式内存对象缓存系统,主要用于加速动态Web应用,通过减少数据库负载来提高响应速度。它支持多种编程语言,易于集成到现有应用中。
- CacheManager:是一个通用的缓存库,支持多种缓存提供者,包括内存缓存、Redis、Memcached 等。它提供了统一的接口和配置方式,方便开发者在不同的缓存提供者之间切换。
- NCache:是一个商业级的分布式缓存解决方案,提供高性能、可扩展的缓存服务。NCache 支持 .NET 应用,可以在分布式环境中提供一致性和高可用性,适合大规模企业应用。
以上五个缓存组件是.NET中常用的缓存解决方案。根据这五种缓存组件的特点和适用场景,我们决定在孢子记账项目的当前阶段使用 Redis
作为缓存的解决方案,因为它支持分布式缓存,能够满足孢子记账的高并发访问需求,同时提供了丰富的缓存功能,如数据过期、持久化等。在后续的项目阶段,我们将引入CacheManager
,以便在不同的缓存提供者之间切换,进一步提高系统的灵活性和可扩展性。
五、孢子记账的缓存实现
在孢子记账项目中,我们将使用 Redis
作为缓存解决方案。首先,我们需要安装 StackExchange.Redis
NuGet 包,这是一个高性能的 Redis 客户端库,支持 .NET 应用程序与 Redis 服务器进行交互。这一步我们已经在增加公共代码这一节中完成了,有不清楚的可以返回查看。
5.1 业务分析
在使用缓存之前,我们需要分析孢子记账项目中哪些数据适合缓存。根据项目的业务需求,我们可以分析出来以下几类数据适合缓存:
- 用户Token:用户登录后生成的Token可以缓存,以便在后续请求中快速验证用户身份,并可以设置过期时间,以此判断token是否过期。
- 验证码:验证码通常是短期有效的,可以缓存一段时间,来实现快速验证用户输入的验证码是否正确,以及验证码是否过期(在身份认证这一篇文章中已实现)。
- 币种:币种信息通常不会频繁变动,可以缓存以减少对数据库的访问。
- 收支分类:收支分类信息通常是相对静态的,可以缓存以提高查询效率。
- 汇率:汇率信息可能会频繁变动,但可以设置一个较短的过期时间进行缓存,以减少对数据库的访问。
- 用户配置:用户的个性化配置可以缓存,以便快速查询用户设置。
- 用户角色:用户角色信息通常不会频繁变动,可以缓存以提高查询效率。
目前看来,以上这些数据是孢子记账项目中适合缓存的数据,后续如果有其他数据需要缓存,我们会在相应的业务逻辑中进行实现。
看到这里,可能有些同学会问,为什么不直接将所有数据都缓存起来呢?这是因为缓存虽然可以提高性能,但也会带来一些问题,比如缓存的容量有限,缓存数据的过期和失效等。因此,我们需要根据业务需求和数据访问频率来选择合适的数据进行缓存,以达到最佳的性能优化效果。
那么,我们选定这些数据作为缓存数据是以什么为依据的呢?主要是根据以下几个方面进行分析的。首先,如果某些数据被频繁访问,那么将这些数据缓存起来可以减少对数据库的访问(比如汇率、收支分类等),提高系统性能。其次,如果某些数据的计算开销较大,那么将计算结果缓存起来可以避免重复计算(目前暂时没有),提高响应速度。最后,如果某些数据的变化频率较低(例如币种、用户角色、用户配置),那么将这些数据缓存起来可以减少对数据库的压力,提高系统的可用性。
5.2 业务代码调用
我们以 用户配置 为例,来演示如何在孢子记账项目中使用缓存,其他数据的缓存实现类似,请大家自己动手实现。
首先,我们需要将封装好的Redis类注入到SP.ConfigService
微服务中,在Program.cs
文件中添加以下代码:
// more code ...// 引入必要的命名空间
using SP.Common.Redis;// more code ...// 注册Redis服务
builder.Services.AddRedisService(builder.Configuration);// more code ...
接下来,我们需要在SP.ConfigService
微服务中需要用到缓存的地方,注入IRedisService
接口,并使用它来进行缓存操作。以下是一个示例代码片段,展示了如何在ConfigController
中使用缓存来获取用户配置:
// more code ...
using SP.Common.Redis;namespace SP.ConfigService.Service.Impl;/// <summary>
/// 配置服务实现类
/// </summary>
public class ConfigServerImpl : IConfigServer
{/// more code .../// <summary>/// Redis服务/// </summary>private readonly IRedisService _redisService;/// <summary>/// 用户配置key/// </summary>private readonly string _redisUserConfigKey;/// <summary>/// 配置服务实现类构造函数/// </summary>/// <param name="context">配置服务数据库上下文</param>/// <param name="contextSession">上下文会话</param>/// <param name="mapper">自动映射器</param>/// <param name="redisService">Redis服务</param>public ConfigServerImpl(ConfigServiceDbContext context, ContextSession contextSession, IMapper mapper,IRedisService redisService){_redisService = redisService;// more code ..._redisUserConfigKey = ConfigRedisKey.UserConfig;_redisUserConfigKey = string.Format(_redisUserConfigKey, _userId);}/// <summary>/// 查询用户配置/// </summary>/// <returns>用户配置</returns>public async Task<List<ConfigResponse>> GetConfig(){List<ConfigResponse>? config = await _redisService.GetAsync<List<ConfigResponse>>(_redisUserConfigKey);if (config != null){// 如果redis中存在配置,则直接返回return config;}// 查询用户配置List<Config> configs = _context.Configs.Where(c => c.UserId == _userId).ToList();// more code ...// 将配置实体转换为响应模型List<ConfigResponse> configResponses = _autoMapper.Map<List<ConfigResponse>>(configs);// 将查询出来的数据缓存到Redis中,存储时间为24小时await _redisService.SetAsync(_redisUserConfigKey, configResponses, 60 * 60 * 24);return configResponses;}/// <summary>/// 更新用户配置/// </summary>/// <param name="config">配置更新请求</param>public async Task UpdateConfig(ConfigEditRequest config){// more code ...// 删除Redis缓存await _redisService.RemoveAsync(_redisUserConfigKey);}/// <summary>/// 设置用户默认货币/// </summary>/// <param name="userId"></param>/// <param name="defaultCurrencyId"></param>/// <returns></returns>public async Task SetUserDefaultCurrencyAsync(long userId, string defaultCurrencyId){// more code ...// 删除Redis缓存await _redisService.RemoveAsync(_redisUserConfigKey);}
}
在上面的代码中,我们首先注入了IRedisService
接口,并在构造函数中初始化了Redis服务。然后,在GetConfig
方法中,我们首先尝试从Redis缓存中获取用户配置,如果缓存中存在,则直接返回;如果缓存中不存在,则查询数据库,并将查询结果存储到Redis缓存中,设置过期时间为24小时。在UpdateConfig
方法中,我们在更新用户配置后,删除了Redis缓存,以确保下次查询时能够获取到最新的配置。
那么,为什么我们要设置缓存的过期时间呢?这是因为缓存数据可能会过期或失效,如果不设置过期时间,缓存中的数据可能会一直存在,导致缓存数据不一致或过时。因此,我们需要根据业务需求设置合适的过期时间,以确保缓存数据的有效性和一致性。
在代码中,我们看到使用了ConfigRedisKey
类,它是一个用于生成Redis缓存键的工具类。我们可以在这个类中定义一些常量或静态方法来生成不同的缓存键,以便在不同的业务逻辑中使用。ConfigRedisKey
类位于SP.ConfigService
微服务下Models\Enumeration
目录中,以下是这个类的代码:
namespace SP.ConfigService.Models.Enumeration;/// <summary>
/// 配置Redis键枚举
/// </summary>
public class ConfigRedisKey
{/// <summary>/// 用户配置的Redis键/// </summary>public const string UserConfig = "UserConfig:{0}";
}
在这个类中,我们定义了一个常量UserConfig
,它表示用户配置的Redis键。我们使用占位符{0}
来表示用户ID,这样可以根据不同的用户生成唯一的缓存键。
我建议在需要缓存的每个微服务中都创建一个类似的RedisKey
类,用于生成不同的缓存键。这样可以提高代码的可读性和可维护性,避免在业务逻辑中硬编码缓存键。如果某个键在多个微服务中都需要使用,可以将它提取到SP.Common.Redis
命名空间下的公共类中,以便在多个微服务中共享。
5.3 nacos配置
在孢子记账项目中,我们使用 Nacos
作为配置中心来管理微服务的配置。为了使缓存服务能够正确连接到 Redis,我们需要在 Nacos 中添加相应的配置项。打开Nacos 界面,进入 Common
微服务的配置管理页面,添加以下配置项:
{"Redis": {"ConnectionString": "14.103.224.141:6379,password=123*asdasd","DefaultDatabase": 0,"ConnectTimeout": 5000,"DefaultExpireSeconds": 3600}
}
这样就完成了孢子记账项目中用户配置的缓存实现。其他数据的缓存实现类似,只需根据具体业务需求调整缓存的键名和过期时间即可,这里就不再一一列举了。
六、总结
通过使用缓存,我们可以显著提高孢子记账项目的性能,减少对数据库的访问压力。缓存可以存储经常使用的数据,避免重复查询数据库,从而提高系统响应速度和用户体验。在实际应用中,我们需要根据业务需求和数据访问频率来选择合适的数据进行缓存,并合理设置缓存的过期时间和失效策略,以确保缓存数据的有效性和一致性。