.NET Core 6中Hazelcast内存分布式缓存实现正确性与性能优化问询
嗨,我来帮你梳理下这个Hazelcast实现的问题,先直接回应你的核心疑问:Hazelcast本身就是内存分布式缓存,你的数据默认就存储在集群内存中,不需要额外配置就能实现内存缓存能力。不过你的代码里存在不少影响性能的问题,这应该就是测试时Hazelcast比MSSQL慢的主要原因,下面我逐点分析并给出优化建议:
1. 最影响性能的问题:重复创建Hazelcast客户端
你的每个缓存写入方法(CacheStudentListAsync、CacheSubjectListAsync等)和查询方法(LoadStudentsWithHighestMarksAsync),每次调用都会创建新的HazelcastClient实例。客户端初始化需要建立连接、完成认证、协商协议,这些操作开销极大,重复创建会严重拖慢性能。
Hazelcast客户端是线程安全的,正确的做法是在整个应用生命周期内复用一个单例客户端:
- 修改DI注册逻辑,直接注入单例的
IHazelcastClient:
public static void AddCacheService(this IServiceCollection services, HazelcastOptions hazelcastOptions) { // 注册单例客户端,使用异步工厂模式 services.AddSingleton(async sp => await HazelcastClientFactory.StartNewClientAsync(hazelcastOptions)); services.AddSingleton<IHazelcastStudentHelper, HazelcastStudentHelper>(); }
- 在
HazelcastStudentHelper中直接注入客户端,不再手动创建:
private readonly IHazelcastClient _client; private const string _mapStudent = "students"; private const string _mapSubject = "subjects"; private const string _mapExam = "exams"; private const string _mapMark = "marks"; public HazelcastStudentHelper(IHazelcastClient client) { _client = client; }
2. 重复初始化索引与SQL映射
你每次调用缓存写入方法时,都会执行AddIndexAsync和CREATE OR REPLACE MAPPING操作。这些初始化操作只需要执行一次(比如应用启动时或第一次使用Map时),重复执行会浪费资源甚至锁定Map,影响并发性能。
可以用惰性初始化+线程安全的方式确保只初始化一次:
// 记录各个Map的初始化状态,确保线程安全 private static readonly ConcurrentDictionary<string, bool> _mapInitStatus = new ConcurrentDictionary<string, bool>(); private async Task EnsureStudentMapInitializedAsync(CancellationToken token) { if (_mapInitStatus.TryGetValue(_mapStudent, out var initialized) && initialized) return; await using var map = await _client.GetMapAsync<long, HazelcastJsonValue>(_mapStudent, token); // 创建索引 await map.AddIndexAsync(IndexType.Sorted, "__key", token); await map.AddIndexAsync(IndexType.Hashed, "Name", token); // 创建SQL映射 await _client.Sql.ExecuteCommandAsync($@" CREATE OR REPLACE MAPPING {_mapStudent} ( __key BIGINT, Id BIGINT, Name VARCHAR, RollNumber VARCHAR, CreatedAt TIMESTAMP WITH TIME ZONE, ModifiedAt TIMESTAMP WITH TIME ZONE) TYPE IMap OPTIONS ('keyFormat'='bigint', 'valueFormat'='json-flat')", token); _mapInitStatus.TryAdd(_mapStudent, true); }
然后在CacheStudentListAsync开头先调用这个初始化方法,其他Map同理实现对应的初始化逻辑。
3. 手动序列化的冗余与优化
你当前手动用JsonSerializer序列化实体,再转成HazelcastJsonValue,这不仅增加了代码冗余,还可能带来不必要的性能开销。Hazelcast客户端支持直接序列化.NET对象,比如使用Compact序列化(性能更优)或默认的JSON序列化:
- 在
appsettings.json中配置Compact序列化:
"Hazelcast": { "Networking": { "Addresses": [ "localhost:5701" ], "Authentication": { "Token": "my-token" } }, "Serialization": { "Compact": { "Enabled": true, "SerializerDefinitions": [ { "Type": "YourNamespace.Student, YourAssemblyName", "SerializerType": "Hazelcast.Serialization.Compact.DefaultCompactSerializer, Hazelcast.Net" }, { "Type": "YourNamespace.Subject, YourAssemblyName", "SerializerType": "Hazelcast.Serialization.Compact.DefaultCompactSerializer, Hazelcast.Net" } // 其他实体类同理添加 ] } } }
- 之后你可以直接使用
IMap<long, Student>,省去手动序列化的步骤,代码更简洁,性能也更好。
4. 查询性能优化
你的SQL查询逻辑可以进一步优化,同时确保索引正确生效:
- 子查询
(SELECT MIN(ExamCount) AS MinExamCount FROM (...))可以考虑提前计算,避免重复扫描数据; - 确保
marks表的StudentId哈希索引、MarkValue排序索引都能被查询用到(可以通过Hazelcast Management Center查看SQL执行计划); - 如果是单节点Hazelcast测试,MSSQL可能因为本地磁盘缓存、成熟的查询优化器显得更快,但在多节点分布式场景下,Hazelcast的内存优势会更明显。
5. 异常处理的改进
你的代码中catch (Exception) { throw; }的写法没有实际意义,反而可能丢失部分堆栈信息。建议要么去掉多余的try-catch,让上层处理异常;要么添加日志记录,方便排查问题:
catch (Exception ex) { // 用你项目中的日志组件记录异常 _logger.LogError(ex, "Failed to cache student list"); throw; // 保留原始堆栈信息 }
再重申一次:Hazelcast默认就是纯内存分布式缓存,你不需要额外配置就能实现内存存储。测试时性能不如MSSQL,主要是因为代码中的重复初始化开销导致的,按照上面的优化点调整后,应该能看到明显的性能提升。
备注:内容来源于stack exchange,提问作者Ebnul Mahmood ShovOn

