1.前言
上一篇我们讨论了云计算设计模式之服务聚合,这一篇我们来讨论下云计算设计模式之读写分离模式.在传统的应用中我们通过DBContext操作数据库都是在同一个Repository中对数据执行CRUD操作.我们都知道,数据库读写操作的I/O效率有较大差异的.发送到数据库的读写请求,会因为某些耗时的写入操作影响读取数据操作的效率,从而降低了系统吞吐量.另外,从安全的角度来说,读写采用同一个接口,也不利于我们通过权限控制数据读写操作,一旦用户非法拥有调用数据读写接口的权限,可以造成很大的破坏,而如果做了使用了读写分离模式,这种概率就减少一半.
2.读写分离
在数据库高可用方案的发展过程中,首先是手动地做数据库备份,后来是数据库镜像,主数据库挂了,镜像数据库会立即代替主数据库提供服务,最终发展成SqlAlwaysOn.SqlAlwaysOn就是做了读写分离的.这是SQL Server数据库提供的读写分离设计.
从软件架构的角度来说,设计不同的接口来完成数据读取和数据更新操作,更加方便扩展,容易满足业务快速发展的需求.传统的应用中,我们使用DTO来存放临时数据,如果考虑并发的话,很可能会出现数据不一致的情形,即对DTO的修改还没反映到数据库,另一个线程就从数据库里面读取数据,这时候读取的数据已经是脏数据了.
读写隔离的模式就是要使用隔离的接口来操作数据,下图展示了这种设计思想.
这时候在数据库中我们读取和操作的是同一个数据库,如果数据没有锁机制,那么还是可能发生脏读,如果有锁机制,那么还是会发生阻塞.如果从不同的数据源来读写就不会有这种问题了,那么数据源之间就必须有同步机制了.如下图所示
3.Example
1)读模型
// Query interface namespace ReadModel { public interface ProductsDao { ProductDisplay FindById(int productId); IEnumerable<ProductDisplay> FindByName(string name); IEnumerable<ProductInventory> FindOutOfStockProducts(); IEnumerable<ProductDisplay> FindRelatedProducts(int productId); } public class ProductDisplay { public int ID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal UnitPrice { get; set; } public bool IsOutOfStock { get; set; } public double UserRating { get; set; } } public class ProductInventory { public int ID { get; set; } public string Name { get; set; } public int CurrentStock { get; set; } } }为了接下来定义更新操作,先定义Commond相关接口及实现接口的类.
public interface Icommand { Guid Id { get; } } public class RateProduct : Icommand { public RateProduct() { this.Id = Guid.NewGuid(); } public Guid Id { get; set; } public int ProductId { get; set; } public int rating { get; set; } public int UserId {get; set; } }系统使用一个Handler类来执行应用提交的更新请求,根据以往我们了解到的云计算的设计经验,我们会把请求放入消息队列,然后使用消费者进行逐一处理.示例如下:
public class ProductsCommandHandler : ICommandHandler<AddNewProduct>, ICommandHandler<RateProduct>, ICommandHandler<AddToInventory>, ICommandHandler<ConfirmItemShipped>, ICommandHandler<UpdateStockFromInventoryRecount> { private readonly IRepository<Product> repository; public ProductsCommandHandler (IRepository<Product> repository) { this.repository = repository; } void Handle (AddNewProduct command) { ... } void Handle (RateProduct command) { var product = repository.Find(command.ProductId); if (product != null) { product.RateProuct(command.UserId, command.rating); repository.Save(product); } } void Handle (AddToInventory command) { ... } void Handle (ConfirmItemsShipped command) { ... } void Handle (UpdateStockFromInventoryRecount command) { ... } }那么在业务逻辑层,我们如何设计接口,请参考如下范例:
public interface ProductsDomain { void AddNewProduct(int id, string name, string description, decimal price); void RateProduct(int userId int rating); void AddToInventory(int productId, int quantity); void ConfirmItemsShipped(int productId, int quantity); void UpdateStockFromInventoryRecount(int productId, int updatedQuantity); }4.相关阅读
The following patterns and guidance may also be relevant when implementing this pattern:
Data Consistency Primer. This guidance explains the issues that are typically encountered due to eventual consistency between the read and write data stores when using the CQRS pattern, and how these issues can be resolved.
Data Partitioning Guidance. This guidance describes how the read and write data stores used in the CQRS pattern can be divided into separate partitions that can be managed and accessed separately to improve scalability, reduce contention, and optimize performance.
Event Sourcing Pattern. This pattern describes in more detail how Event Sourcing can be used with the CQRS pattern to simplify tasks in complex domains; improve performance, scalability, and responsiveness; provide consistency for transactional data; and maintain full audit trails and history that may enable compensating actions.
Materialized View Pattern. The read model of a CQRS implementation may contain materialized views of the write model data, or the read model may be used to generate materialized views.