C#单例缓存层数据拷贝策略及增删操作锁实现方案咨询
问题解答
一、拷贝策略设计方案
你需要从集合拷贝+元素拷贝两层处理,避免下游直接修改缓存内的原始数据:
- 拷贝分层逻辑
- 如果
UDT是不可变类(所有属性仅构造时赋值,无公共setter),只需要对集合做浅拷贝:返回时直接调用list.ToList()生成新的集合实例即可,避免下游对集合做增删改操作影响缓存。 - 如果
UDT是可变类(属性可修改),必须对每个元素做深拷贝:可以给UDT实现拷贝构造方法、ICloneable接口,或者用序列化/表达式树实现通用深拷贝逻辑,返回的集合内每个元素都是独立副本,下游修改元素属性不会影响缓存。
- 如果
- 优化建议
- 可以在返回方法里加可选参数控制是否返回副本,比如
public async Task<List<UDT>> GetUDTsForUser(string id, bool fetchFromDB = false, bool returnCopy = true),内部调用不需要拷贝时可以传false节省性能。 - 从DB加载数据写入缓存时不需要做拷贝,仅在对外返回数据时生成副本即可。
GetUDTForUser方法返回单个元素时也需要返回拷贝后的对象,不能直接返回缓存内的原始元素引用。
- 可以在返回方法里加可选参数控制是否返回副本,比如
二、Add/Remove操作加锁方案
必须加锁的原因
你当前用的List<T>不是线程安全类型,且MyCache是单例服务,多线程并发修改同一个用户的UDT列表时,会出现数据丢失、遍历异常等问题,所以增删操作必须加锁。
正确实现方式
- 统一用你已经定义的全局
_mutex锁(如果业务并发量不高,全局锁足够用;如果追求更高并发可以改成分组细粒度锁,按用户id/GroupId加锁)包裹所有修改缓存的逻辑,修改后的代码示例如下:
// AddElement改成异步方法,统一用AsyncLock public async Task AddElement(UDT udt) { if (udt == null) return; using (await _mutex.LockAsync()) { var udtList = Cache.GetOrCreate(udt.GroupId, entry => { return new List<UDT>(); }); // 不要用默认的Contains引用比较,用Id判断更准确 if (!udtList.Any(u => u.Id == udt.Id)) { udtList.Add(udt); // 引用类型修改后不需要再调用Set,缓存内的List已经同步更新,此行可删 // Cache.Set(udt.GroupId, udtList); } } } // RemoveElement同理加锁 public async Task RemoveElement(string groupId, long udtId) { using (await _mutex.LockAsync()) { var udtList = Cache.Get<List<UDT>>(groupId); if (udtList != null) { udtList.RemoveAll(e => e.Id == udtId); // 同上,不需要重复Set // Cache.Set(groupId, udtList); } } }
- 其他优化点
- 把
public MemoryCache Cache { get; set; }改成私有属性,禁止外部直接操作缓存实例,避免不可控的缓存修改。 - 统一两个Get方法的DB查询逻辑,当前
GetUDTForUser没有fetchFromDB参数,不管什么情况都会查DB,逻辑和GetUDTsForUser不一致,建议对齐参数。
- 把
内容的提问来源于stack exchange,提问作者dtln812
相关产品推荐
相关产品推荐

