Blazor Server应用中基于Entity Framework实现多用户共享读写数据的方案咨询
Blazor Server 多用户共享内存数据 + EF 持久化方案实现
你的方案完全可行,尤其适合你提到的少量并发冲突可接受的场景,核心思路「内存缓存共享数据+定期/按需持久化」刚好匹配Blazor Server的会话特性和EF的持久化能力。下面是结合Blazor和EF的简便实现方法:
1. 内存共享数据管理:单例服务
创建一个单例的共享数据管理服务,用来按分组(如部门)缓存数据并管理生命周期:
- 定义一个
CachedData<T>类,封装实体数据及生命周期信息:public class CachedData<T> { public T Data { get; set; } public DateTime LastAccessed { get; set; } public int UserCount { get; set; } } - 实现
SharedDataManager<T>单例服务,用ConcurrentDictionary保证线程安全:public class SharedDataManager<T> where T : class { private readonly ConcurrentDictionary<string, CachedData<T>> _cache = new(); private readonly IDbContextFactory<YourDbContext> _dbContextFactory; private readonly Timer _persistTimer; public SharedDataManager(IDbContextFactory<YourDbContext> dbContextFactory) { _dbContextFactory = dbContextFactory; // 启动定时持久化任务,示例为每5分钟执行一次 _persistTimer = new Timer(async _ => await PersistIdleDataAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(5)); } // 获取或加载分组数据 public async Task<T> GetOrLoadDataAsync(string groupKey, Func<YourDbContext, Task<T>> loadFunc) { if (_cache.TryGetValue(groupKey, out var cachedItem)) { cachedItem.LastAccessed = DateTime.Now; cachedItem.UserCount++; return cachedItem.Data; } // 从数据库加载数据(使用新的DbContext实例) using var dbContext = _dbContextFactory.CreateDbContext(); var data = await loadFunc(dbContext); var newCachedItem = new CachedData<T> { Data = data, LastAccessed = DateTime.Now, UserCount = 1 }; _cache.TryAdd(groupKey, newCachedItem); return data; } // 减少用户计数(组件销毁时调用) public void DecrementUserCount(string groupKey) { if (_cache.TryGetValue(groupKey, out var cachedItem)) { cachedItem.UserCount--; cachedItem.LastAccessed = DateTime.Now; } } // 手动保存数据到数据库 public async Task SaveDataAsync(string groupKey) { if (_cache.TryGetValue(groupKey, out var cachedItem)) { using var dbContext = _dbContextFactory.CreateDbContext(); dbContext.Set<T>().Update(cachedItem.Data); await dbContext.SaveChangesAsync(); cachedItem.LastAccessed = DateTime.Now; } } // 持久化闲置数据(超时或无用户访问) private async Task PersistIdleDataAsync() { var idleKeys = _cache.Where(kv => kv.Value.UserCount == 0 || DateTime.Now - kv.Value.LastAccessed > TimeSpan.FromMinutes(30)) .Select(kv => kv.Key) .ToList(); foreach (var key in idleKeys) { if (_cache.TryRemove(key, out var cachedItem)) { using var dbContext = _dbContextFactory.CreateDbContext(); dbContext.Set<T>().Update(cachedItem.Data); await dbContext.SaveChangesAsync(); } } } } - 在
Program.cs中注册为单例:builder.Services.AddSingleton(typeof(SharedDataManager<>));
2. 实时UI更新:状态同步
利用Blazor的组件生命周期和事件订阅实现UI自动刷新:
- 让共享的实体类实现
INotifyPropertyChanged接口,触发属性变更通知:public class DepartmentData : INotifyPropertyChanged { private string _name; public string Name { get => _name; set { if (_name != value) { _name = value; OnPropertyChanged(nameof(Name)); } } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } - 在Blazor组件中注入服务并订阅变更:
@inject SharedDataManager<DepartmentData> DataManager @implements IDisposable <input @bind="DepartmentData.Name" @bind:event="oninput" placeholder="部门名称" /> <button @onclick="SaveChanges" class="ml-2">手动保存</button> @code { private DepartmentData DepartmentData { get; set; } private string _groupKey; // 从用户部门属性动态获取,示例固定为"Dept001" protected override async Task OnInitializedAsync() { _groupKey = "Dept001"; DepartmentData = await DataManager.GetOrLoadDataAsync(_groupKey, db => db.DepartmentData.AsNoTracking().FirstAsync(d => d.Id == _groupKey)); DepartmentData.PropertyChanged += OnDataChanged; } private void OnDataChanged(object sender, PropertyChangedEventArgs e) { StateHasChanged(); } private async Task SaveChanges() { await DataManager.SaveDataAsync(_groupKey); } public void Dispose() { DepartmentData.PropertyChanged -= OnDataChanged; DataManager.DecrementUserCount(_groupKey); } }
3. EF 使用关键注意事项
- 绝对不要在单例服务中持有DbContext实例,必须通过
IDbContextFactory每次创建新的上下文,避免线程安全问题和跟踪冲突。 - 加载数据时建议使用
AsNoTracking(),因为内存中的数据是脱离EF跟踪的,后续更新时直接调用Update()即可。
额外提示
- 如果想进一步减少冲突,可以给实体加
Version字段,启用EF的乐观锁:[Timestamp] public byte[] Version { get; set; } - 可根据业务需求调整缓存超时时间和定时持久化间隔。
内容的提问来源于stack exchange,提问作者DevDevDev
相关产品推荐
相关产品推荐

