一、为什么需要专门设计只读仓储
在日常开发中,我们经常会遇到这样的场景:系统需要展示大量数据,但这些数据很少被修改。比如电商网站的商品列表、新闻网站的文章列表等。如果每次都从主数据库中查询,会给数据库带来不必要的压力。
这时候,只读仓储就派上用场了。它专门为查询操作优化,与常规的仓储(负责增删改查)分开设计。这样做有几个明显好处:
- 查询性能更好,因为只读仓储可以针对查询做特殊优化
- 减轻主数据库负担,提高系统整体稳定性
- 可以灵活选择更适合查询的存储方案
举个例子,我们有个新闻发布系统,99%的请求都是查看新闻,只有1%是编辑新闻。如果所有请求都走同一个仓储,显然不太合理。
二、只读仓储的三种实现方式
1. 数据库视图方式
技术栈:C# + Entity Framework Core
// 新闻实体
public class News
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime PublishTime { get; set; }
public bool IsDeleted { get; set; }
}
// 只读仓储实现
public class NewsReadOnlyRepository
{
private readonly AppDbContext _context;
public NewsReadOnlyRepository(AppDbContext context)
{
_context = context;
}
// 使用数据库视图查询
public async Task<List<News>> GetHotNewsAsync(int count)
{
return await _context.NewsView
.Where(n => !n.IsDeleted)
.OrderByDescending(n => n.PublishTime)
.Take(count)
.ToListAsync();
}
}
这种方式适合数据量不是特别大的场景。优点是实现简单,缺点是性能提升有限。
2. 缓存结合方式
技术栈:C# + Redis
public class CachedNewsRepository
{
private readonly NewsRepository _newsRepo;
private readonly IDistributedCache _cache;
public CachedNewsRepository(NewsRepository newsRepo, IDistributedCache cache)
{
_newsRepo = newsRepo;
_cache = cache;
}
public async Task<List<News>> GetLatestNewsAsync()
{
// 先从缓存读取
var cacheKey = "latest_news";
var cachedData = await _cache.GetStringAsync(cacheKey);
if (!string.IsNullOrEmpty(cachedData))
{
return JsonSerializer.Deserialize<List<News>>(cachedData);
}
// 缓存没有再从数据库读取
var news = await _newsRepo.GetLatestNewsAsync();
// 设置缓存,过期时间5分钟
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
};
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(news),
options);
return news;
}
}
这种方式适合读多写少的场景。优点是性能提升明显,缺点是需要处理缓存一致性问题。
3. 专门查询数据库方式
技术栈:C# + Elasticsearch
public class NewsSearchRepository
{
private readonly ElasticClient _client;
public NewsSearchRepository(ElasticClient client)
{
_client = client;
}
public async Task<List<News>> SearchNewsAsync(string keyword)
{
var response = await _client.SearchAsync<News>(s => s
.Query(q => q
.MultiMatch(m => m
.Fields(f => f
.Field(n => n.Title)
.Field(n => n.Content))
.Query(keyword)
)
)
.Size(100)
);
return response.Documents.ToList();
}
}
这种方式适合需要复杂查询的场景。优点是查询性能极佳,缺点是同步数据需要额外工作。
三、优化查询性能的实用技巧
1. 合理设计查询模型
不要直接使用领域模型作为查询返回结果。设计专门的DTO模型,只包含查询需要的字段。
// 专门为列表查询设计的DTO
public class NewsListItemDto
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime PublishTime { get; set; }
public string Summary { get; set; }
}
// 在仓储中转换
public async Task<List<NewsListItemDto>> GetNewsListAsync(int page, int pageSize)
{
return await _context.News
.Where(n => !n.IsDeleted)
.OrderByDescending(n => n.PublishTime)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(n => new NewsListItemDto
{
Id = n.Id,
Title = n.Title,
PublishTime = n.PublishTime,
Summary = n.Content.Length > 100
? n.Content.Substring(0, 100) + "..."
: n.Content
})
.ToListAsync();
}
2. 使用分页查询
永远不要一次性查询大量数据。即使需要全部数据,也建议使用分批次查询。
public async Task<List<News>> GetAllNewsInBatchesAsync(int batchSize = 100)
{
var result = new List<News>();
int offset = 0;
while (true)
{
var batch = await _context.News
.Where(n => !n.IsDeleted)
.OrderBy(n => n.Id)
.Skip(offset)
.Take(batchSize)
.ToListAsync();
if (!batch.Any())
break;
result.AddRange(batch);
offset += batchSize;
}
return result;
}
3. 合理使用索引
确保查询用到的字段都有适当的索引。可以通过EF Core的配置来添加索引。
// 在DbContext中配置索引
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<News>()
.HasIndex(n => n.PublishTime)
.IsDescending();
modelBuilder.Entity<News>()
.HasIndex(n => new { n.IsDeleted, n.PublishTime });
}
四、实际应用中的注意事项
数据一致性问题:使用缓存或专门查询数据库时,要考虑数据同步延迟。对于强一致性要求的场景,需要特殊处理。
查询复杂度控制:不要让单个查询做太多事情。复杂的查询应该拆分成多个简单查询。
监控和调优:对查询性能进行监控,及时发现慢查询并优化。
测试环境差异:注意开发环境和生产环境的数据量差异,小数据量下表现良好的查询,可能在真实环境中变慢。
过度优化陷阱:不是所有查询都需要优化,优先优化真正影响性能的关键查询。
五、总结
只读仓储是DDD中一个非常实用的设计模式,特别适合读多写少的应用场景。通过将读写分离,我们可以针对查询操作做专门的优化,显著提升系统性能。
实现方式上,可以根据实际需求选择数据库视图、缓存或专门查询数据库等不同方案。每种方案都有其适用场景和优缺点,需要根据业务特点和技术条件来选择。
优化查询性能时,要特别注意查询模型设计、分页处理和索引使用等关键点。同时,实际应用中要处理好数据一致性、监控调优等问题。
记住,没有放之四海而皆准的方案。最好的设计永远是适合你当前业务需求和技术条件的设计。希望这些实践经验能帮助你在项目中更好地应用DDD,构建高性能的系统。
评论