Skip to content

Commit a547f48

Browse files
committed
Optimized cache-related code to fix database cache penetration issue
1 parent a82ff5b commit a547f48

File tree

10 files changed

+170
-68
lines changed

10 files changed

+170
-68
lines changed

OpenBioCardServer/Configuration/CacheSettings.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,29 @@ public class CacheSettings
55
public const string SectionName = "CacheSettings";
66

77
public bool Enabled { get; set; } = true;
8+
9+
// Redis
810
public bool UseRedis { get; set; } = false;
911
public string? RedisConnectionString { get; set; }
1012
public string InstanceName { get; set; } = "OpenBioCard:";
13+
14+
// Memory
1115
public long? CacheSizeLimit { get; set; }
16+
public double CompactionPercentage { get; set; } = 0.2;
17+
1218
public int ExpirationMinutes { get; set; } = 30;
1319
public int SlidingExpirationMinutes { get; set; } = 5;
14-
public double CompactionPercentage { get; set; } = 0.2;
20+
21+
// 工厂软超时 (毫秒)
22+
public int FactorySoftTimeoutMilliseconds { get; set; } = 500;
23+
24+
// 故障兜底 (Fail-Safe) 配置
25+
public bool EnableFailSafe { get; set; } = true;
26+
public int FailSafeMaxDurationMinutes { get; set; } = 120;
27+
28+
// 故障节流时间 (秒)
29+
public int FailSafeThrottleDurationSeconds { get; set; } = 30;
30+
31+
// 分布式缓存熔断时间 (秒)
32+
public int DistributedCacheCircuitBreakerDurationSeconds { get; set; } = 2;
1533
}

OpenBioCardServer/Configuration/CacheSettingsValidator.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,43 @@ public ValidateOptionsResult Validate(string? name, CacheSettings options)
1414
var failures = new List<string>();
1515

1616
if (options.CacheSizeLimit <= 0)
17-
{
1817
failures.Add("CacheSizeLimit must be greater than 0.");
19-
}
2018

2119
if (options.ExpirationMinutes <= 0)
22-
{
2320
failures.Add("ExpirationMinutes must be greater than 0.");
24-
}
2521

2622
if (options.SlidingExpirationMinutes <= 0)
27-
{
2823
failures.Add("SlidingExpirationMinutes must be greater than 0.");
29-
}
3024

3125
if (options.CompactionPercentage < 0 || options.CompactionPercentage > 1)
32-
{
3326
failures.Add("CompactionPercentage must be between 0 and 1.");
34-
}
3527

36-
if (options.UseRedis && string.IsNullOrWhiteSpace(options.RedisConnectionString))
28+
if (options.FactorySoftTimeoutMilliseconds <= 0)
29+
failures.Add("FactorySoftTimeoutMilliseconds must be greater than 0.");
30+
31+
if (options.EnableFailSafe)
3732
{
38-
failures.Add("RedisConnectionString cannot be empty when UseRedis is true.");
33+
if (options.FailSafeMaxDurationMinutes <= 0)
34+
failures.Add("FailSafeMaxDurationMinutes must be greater than 0 when FailSafe is enabled.");
35+
36+
if (options.FailSafeThrottleDurationSeconds <= 0)
37+
failures.Add("FailSafeThrottleDurationSeconds must be greater than 0 when FailSafe is enabled.");
3938
}
4039

41-
if (options.UseRedis && string.IsNullOrWhiteSpace(options.InstanceName))
40+
if (options.DistributedCacheCircuitBreakerDurationSeconds <= 0)
41+
failures.Add("DistributedCacheCircuitBreakerDurationSeconds must be greater than 0.");
42+
43+
if (options.UseRedis)
4244
{
43-
failures.Add("InstanceName cannot be empty when UseRedis is true.");
45+
if (string.IsNullOrWhiteSpace(options.RedisConnectionString))
46+
failures.Add("RedisConnectionString cannot be empty when UseRedis is true.");
47+
48+
if (string.IsNullOrWhiteSpace(options.InstanceName))
49+
failures.Add("InstanceName cannot be empty when UseRedis is true.");
4450
}
4551

4652
return failures.Count > 0
4753
? ValidateOptionsResult.Fail(failures)
4854
: ValidateOptionsResult.Success;
4955
}
50-
}
56+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace OpenBioCardServer.Constants;
2+
3+
public class CacheKeys
4+
{
5+
6+
}

OpenBioCardServer/Controllers/Classic/ClassicSettingsController.cs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using OpenBioCardServer.Models.Enums;
1111
using OpenBioCardServer.Services;
1212
using OpenBioCardServer.Utilities.Mappers;
13+
using ZiggyCreatures.Caching.Fusion;
1314

1415
namespace OpenBioCardServer.Controllers.Classic;
1516

@@ -19,7 +20,7 @@ public class ClassicSettingsController : ControllerBase
1920
{
2021
private readonly AppDbContext _context;
2122
private readonly ClassicAuthService _authService;
22-
private readonly ICacheService _cacheService;
23+
private readonly IFusionCache _cache;
2324
private readonly ILogger<ClassicSettingsController> _logger;
2425

2526
// 缓存 Key 常量
@@ -28,12 +29,12 @@ public class ClassicSettingsController : ControllerBase
2829
public ClassicSettingsController(
2930
AppDbContext context,
3031
ClassicAuthService authService,
31-
ICacheService cacheService,
32+
IFusionCache cache,
3233
ILogger<ClassicSettingsController> logger)
3334
{
3435
_context = context;
3536
_authService = authService;
36-
_cacheService = cacheService;
37+
_cache = cache;
3738
_logger = logger;
3839
}
3940

@@ -46,17 +47,19 @@ public async Task<IActionResult> GetPublicSettings()
4647
{
4748
try
4849
{
49-
var response = await _cacheService.GetOrCreateAsync(PublicSettingsCacheKey, async () =>
50-
{
51-
var settings = await _context.SystemSettings.FindAsync(1);
52-
return new ClassicSystemSettingsResponse
50+
var response = await _cache.GetOrSetAsync<ClassicSystemSettingsResponse>(
51+
PublicSettingsCacheKey,
52+
async (ctx, token) =>
5353
{
54-
Title = settings?.Title ?? "OpenBioCard",
55-
Logo = settings?.LogoType.HasValue == true
56-
? ClassicMapper.AssetToString(settings.LogoType.Value, settings.LogoText, settings.LogoData)
57-
: string.Empty
58-
};
59-
});
54+
var settings = await _context.SystemSettings.FindAsync(new object[] { 1 }, token);
55+
return new ClassicSystemSettingsResponse
56+
{
57+
Title = settings?.Title ?? "OpenBioCard",
58+
Logo = settings?.LogoType.HasValue == true
59+
? ClassicMapper.AssetToString(settings.LogoType.Value, settings.LogoText, settings.LogoData)
60+
: string.Empty
61+
};
62+
});
6063

6164
return Ok(response);
6265
}
@@ -178,7 +181,7 @@ public async Task<IActionResult> UpdateSettings([FromBody] ClassicUpdateSettings
178181
await _context.SaveChangesAsync();
179182

180183
// 更新成功后清除公共设置的缓存以确保前端获取到最新数据
181-
await _cacheService.RemoveAsync(PublicSettingsCacheKey);
184+
await _cache.RemoveAsync(PublicSettingsCacheKey);
182185

183186
_logger.LogInformation("Admin {Username} updated system settings", request.Username);
184187

OpenBioCardServer/Controllers/ProfileController.cs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using OpenBioCardServer.Models.Entities;
77
using OpenBioCardServer.Services;
88
using OpenBioCardServer.Utilities.Mappers;
9+
using ZiggyCreatures.Caching.Fusion;
910

1011
namespace OpenBioCardServer.Controllers;
1112

@@ -15,13 +16,13 @@ public class ProfileController : ControllerBase
1516
{
1617
private readonly AppDbContext _context;
1718
private readonly AuthService _authService;
18-
private readonly ICacheService _cacheService;
19+
private readonly IFusionCache _cacheService;
1920
private readonly ILogger<ProfileController> _logger;
2021

2122
public ProfileController(
2223
AppDbContext context,
2324
AuthService authService,
24-
ICacheService cacheService,
25+
IFusionCache cacheService,
2526
ILogger<ProfileController> logger)
2627
{
2728
_context = context;
@@ -44,21 +45,22 @@ public async Task<ActionResult<ProfileDto>> GetProfile(string username)
4445

4546
try
4647
{
47-
var profileDto = await _cacheService.GetOrCreateAsync(cacheKey, async () =>
48-
{
49-
var profile = await _context.Profiles
50-
.AsNoTracking()
51-
.AsSplitQuery()
52-
.Include(p => p.Contacts)
53-
.Include(p => p.SocialLinks)
54-
.Include(p => p.Projects)
55-
.Include(p => p.WorkExperiences)
56-
.Include(p => p.SchoolExperiences)
57-
.Include(p => p.Gallery)
58-
.FirstOrDefaultAsync(p => p.Username == username);
59-
60-
return profile == null ? null : DataMapper.ToProfileDto(profile);
61-
});
48+
var profileDto = await _cacheService.GetOrSetAsync<ProfileDto?>(
49+
cacheKey,
50+
async (ctx, token) =>
51+
{
52+
var profile = await _context.Profiles
53+
.AsNoTracking()
54+
.AsSplitQuery()
55+
.Include(p => p.Contacts)
56+
.Include(p => p.SocialLinks)
57+
.Include(p => p.Projects)
58+
.Include(p => p.WorkExperiences)
59+
.Include(p => p.SchoolExperiences)
60+
.Include(p => p.Gallery)
61+
.FirstOrDefaultAsync(p => p.Username == username, token); // 传入 token
62+
return profile == null ? null : DataMapper.ToProfileDto(profile);
63+
});
6264

6365
if (profileDto == null)
6466
{

OpenBioCardServer/OpenBioCardServer.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
2626
<PackageReference Include="StackExchange.Redis" Version="2.10.1" />
2727
<PackageReference Include="System.Text.Json" Version="10.0.1" />
28+
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.4.0" />
29+
<PackageReference Include="ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis" Version="2.4.0" />
30+
<PackageReference Include="ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson" Version="2.4.0" />
31+
<PackageReference Include="ZiggyCreatures.FusionCache.Serialization.SystemTextJson" Version="2.4.0" />
2832
</ItemGroup>
2933

3034
<ItemGroup>

OpenBioCardServer/Program.cs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
using OpenBioCardServer.Constants;
1616
using OpenBioCardServer.Interfaces;
1717
using OpenBioCardServer.Structs.ENums;
18+
using ZiggyCreatures.Caching.Fusion;
19+
using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson;
1820

1921
namespace OpenBioCardServer;
2022

@@ -246,21 +248,70 @@ private static void ConfigureCacheService(WebApplicationBuilder builder)
246248
{
247249
var cacheSettings = builder.Configuration.GetSection(CacheSettings.SectionName).Get<CacheSettings>() ?? new CacheSettings();
248250

251+
if (!cacheSettings.Enabled)
252+
{
253+
Console.WriteLine("==> Cache is DISABLED via configuration.");
254+
255+
builder.Services.AddFusionCache()
256+
.WithDefaultEntryOptions(new FusionCacheEntryOptions
257+
{
258+
Duration = TimeSpan.Zero,
259+
IsFailSafeEnabled = false,
260+
FactorySoftTimeout = Timeout.InfiniteTimeSpan,
261+
AllowBackgroundDistributedCacheOperations = false
262+
});
263+
264+
return;
265+
}
266+
249267
builder.Services.AddMemoryCache(options =>
250268
{
251269
options.SizeLimit = cacheSettings.CacheSizeLimit ?? 100;
252270
options.CompactionPercentage = cacheSettings.CompactionPercentage;
253271
});
254272

255-
if (cacheSettings.UseRedis && !string.IsNullOrEmpty(cacheSettings.RedisConnectionString))
273+
var fusionBuilder = builder.Services.AddFusionCache()
274+
.WithOptions(options =>
275+
{
276+
options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(cacheSettings.DistributedCacheCircuitBreakerDurationSeconds);
277+
})
278+
.WithDefaultEntryOptions(new FusionCacheEntryOptions
279+
{
280+
Duration = TimeSpan.FromMinutes(cacheSettings.ExpirationMinutes),
281+
282+
FactorySoftTimeout = TimeSpan.FromMilliseconds(cacheSettings.FactorySoftTimeoutMilliseconds),
283+
284+
IsFailSafeEnabled = cacheSettings.EnableFailSafe,
285+
FailSafeMaxDuration = TimeSpan.FromMinutes(cacheSettings.FailSafeMaxDurationMinutes),
286+
FailSafeThrottleDuration = TimeSpan.FromSeconds(30),
287+
})
288+
.WithSerializer(new FusionCacheSystemTextJsonSerializer());
289+
290+
// 根据配置决定是否启用 Redis 和 Backplane
291+
if (cacheSettings.Enabled && cacheSettings.UseRedis && !string.IsNullOrEmpty(cacheSettings.RedisConnectionString))
256292
{
257293
builder.Services.AddStackExchangeRedisCache(options =>
258294
{
259295
options.Configuration = cacheSettings.RedisConnectionString;
260296
options.InstanceName = cacheSettings.InstanceName;
261297
});
298+
299+
// 将 Redis 注册为 FusionCache 的二级缓存
300+
fusionBuilder.WithRegisteredDistributedCache();
301+
302+
// 注册 Redis Backplane (用于集群缓存同步)
303+
fusionBuilder.WithStackExchangeRedisBackplane(options =>
304+
{
305+
options.Configuration = cacheSettings.RedisConnectionString;
306+
});
307+
308+
Console.WriteLine("==> FusionCache configured with Redis L2 & Backplane.");
309+
}
310+
else
311+
{
312+
Console.WriteLine("==> FusionCache configured in Memory-Only mode.");
262313
}
263-
builder.Services.AddSingleton<ICacheService, CacheService>();
314+
// builder.Services.AddSingleton<ICacheService, CacheService>();
264315
}
265316

266317
private static void ConfigureCompressionService(WebApplicationBuilder builder)

OpenBioCardServer/Services/ClassicProfileService.cs

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,23 @@
33
using OpenBioCardServer.Interfaces;
44
using OpenBioCardServer.Models.DTOs.Classic;
55
using OpenBioCardServer.Utilities.Mappers;
6+
using ZiggyCreatures.Caching.Fusion;
67

78
namespace OpenBioCardServer.Services;
89

910
public class ClassicProfileService
1011
{
1112
private readonly AppDbContext _context;
12-
private readonly ICacheService _cacheService;
13+
private readonly IFusionCache _cache;
1314
private readonly ILogger<ClassicProfileService> _logger;
1415

1516
public ClassicProfileService(
1617
AppDbContext context,
17-
ICacheService cacheService,
18+
IFusionCache cache,
1819
ILogger<ClassicProfileService> logger)
1920
{
2021
_context = context;
21-
_cacheService = cacheService;
22+
_cache = cache;
2223
_logger = logger;
2324
}
2425

@@ -32,21 +33,22 @@ private static string GetProfileCacheKey(string username) =>
3233
{
3334
var cacheKey = GetProfileCacheKey(username);
3435

35-
return await _cacheService.GetOrCreateAsync(cacheKey, async () =>
36-
{
37-
var profile = await _context.Profiles
38-
.AsNoTracking()
39-
.AsSplitQuery()
40-
.Include(p => p.Contacts)
41-
.Include(p => p.SocialLinks)
42-
.Include(p => p.Projects)
43-
.Include(p => p.WorkExperiences)
44-
.Include(p => p.SchoolExperiences)
45-
.Include(p => p.Gallery)
46-
.FirstOrDefaultAsync(p => p.Username == username);
47-
48-
return profile == null ? null : ClassicMapper.ToClassicProfile(profile);
49-
});
36+
return await _cache.GetOrSetAsync<ClassicProfile?>(
37+
cacheKey,
38+
async (ctx, token) =>
39+
{
40+
var profile = await _context.Profiles
41+
.AsNoTracking()
42+
.AsSplitQuery()
43+
.Include(p => p.Contacts)
44+
.Include(p => p.SocialLinks)
45+
.Include(p => p.Projects)
46+
.Include(p => p.WorkExperiences)
47+
.Include(p => p.SchoolExperiences)
48+
.Include(p => p.Gallery)
49+
.FirstOrDefaultAsync(p => p.Username == username, token); // 传入 token
50+
return profile == null ? null : ClassicMapper.ToClassicProfile(profile);
51+
});
5052
}
5153

5254
/// <summary>
@@ -121,7 +123,7 @@ public async Task<bool> UpdateProfileAsync(string username, ClassicProfile reque
121123
await transaction.CommitAsync();
122124

123125
// 4. Invalidate Cache
124-
await _cacheService.RemoveAsync(GetProfileCacheKey(username));
126+
await _cache.RemoveAsync(GetProfileCacheKey(username));
125127

126128
_logger.LogInformation("Profile updated successfully for user: {Username}", username);
127129
return true;

0 commit comments

Comments
 (0)