Skip to content

多研究些架构,少谈些框架(2)-- 微服务和充血模型 #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
JoeCao opened this issue Jun 12, 2017 · 16 comments
Open

Comments

@JoeCao
Copy link
Owner

JoeCao commented Jun 12, 2017

多研究些架构,少谈些框架(1) -- 论微服务架构的核心概念
多研究些架构,少谈些框架(2)-- 微服务和充血模型
多研究些架构,少谈些框架(3)-- 微服务和事件驱动

多研究些架构,少谈些框架(2)-- 微服务和充血模型

2017-6-12 曹祖鹏
上篇我们聊了微服务的DDD之间的关系,很多人还是觉得很虚幻,DDD那么复杂的理论,聚合根、值对象、事件溯源,到底我们该怎么入手呢?
实际上DDD和面向对象设计、设计模式等等理论有千丝万缕的联系,如果不熟悉OOA、OOD,DDD也是使用不好的。不过学习这些OO理论的时候,大家往往感觉到无用武之地,因为大部分的Java程序员开发生涯是从学习J2EE经典的分层理论开始的(Action、Service、Dao),在这种分层理论中,我们基本没有啥机会使用那些所谓的“行为型”的设计模式,这里的核心原因,就是J2EE经典分层的开发方式是“贫血模型”。
Martin Fowler在他的《企业应用架构模式》这本书中提出了两种开发方式“事务脚本”和“领域模型”,这两种开发分别对应了“贫血模型”和“充血模型”。

事务脚本开发模式

  • 事务脚本的核心是过程,可以认为大部分的业务处理都是一条条的SQL,事务脚本把单个SQL组织成为一段业务逻辑,在逻辑执行的时候,使用事务来保证逻辑的ACID。最典型的就是存储过程。当然我们在平时J2EE经典分层架构中,经常在Service层使用事务脚本。
    aa1

使用这种开发方式,对象只用于在各层之间传输数据用,这里的对象就是“贫血模型”,只有数据字段和Get/Set方法,没有逻辑在对象中。

我们以一个库存扣减的场景来举例:

  • 业务场景
    首先谈一下业务场景,一个下订单扣减库存(锁库存),这个很简单
    先判断库存是否足够,然后扣减可销售库存,增加订单占用库存,然后再记录一个库存变动记录日志(作为凭证)
  • 贫血模型的设计
    首先设计一个库存表 Stock,有如下字段
    image

设计一个Stock对象(Getter和Setter省略)

public class Stock {
	private String spuId;
	private String skuId;
	private int stockNum;
	private int orderStockNum;
}
  • Service入口
    设计一个StockService,在其中的lock方法中写逻辑
    入参为(spuId, skuId, num)
    实现伪代码
count = select stocknum from stock where spuId=xx and skuid=xx
if count>num {
     update stock set stocknum=stocknum-num, orderstocknum=orderstocknum+num  where skuId=xx and spuId=xx
} else {
     //库存不足,扣减失败
}
insert stock_log set xx=xx, date= new Date()
  • ok,打完收工,如果做的好一些,可以把update和select count合一,这样可以利用一条语句完成自旋,解决并发问题(高手)。
    小结一下:
    有没有发现,在这个业务领域非常重要的核心逻辑 -- 下订单扣减库存中操作过程中,Stock对象根本不用出现,全部是数据库操作SQL,所谓的业务逻辑就是由多条SQL构成。Stock只是CRUD的数据对象而已,没逻辑可言。

  • 马丁福勒定义的“贫血模型”是反模式,面对简单的小系统用事务脚本方式开发没问题,业务逻辑复杂了,业务逻辑、各种状态散布在大量的函数中,维护扩展的成本一下子就上来,贫血模型没有实施微服务的基础。

  • 虽然我们用Java这样的面向对象语言来开发,但是其实和过程型语言是一样的,所以很多情况下大家用数据库的存储过程来替代Java写逻辑反而效果会更好,(ps:用了Spring boot也不是微服务),

领域模型的开发模式

  • 领域模型是将数据和行为封装在一起,并与现实世界的业务对象相映射。各类具备明确的职责划分,使得逻辑分散到合适对象中。这样的对象就是“充血模型” 。
  • 在具体实践中,我们需要明确一个概念,就是领域模型是有状态的,他代表一个实际存在的事物。还是接着上面的例子,我们设计Stock对象需要代表一种商品的实际库存,并在这个对象上面加上业务逻辑的方法
    InventoryClass

这样做下单锁库存业务逻辑的时候,每次必须先从Repository根据主键load还原Inventory这个对象,然后执行对应的lock(num)方法改变这个Inventory对象的状态(属性也是状态的一种),然后再通过Repository的save方法把这个对象持久化到存储去。
完成上述一系列操作的是Application,Application对外提供了这种集成操作的接口

image
领域模型开发方法最重要的是把扣减造成的状态变化的细节放到了Inventory对象执行,这就是对业务逻辑的封装。
Application对象的lock方法可以和事务脚本方法的StockService的lock来做个对比,StockService是完全掌握所有细节,一旦有了变化(比如库存为0也可以扣减),Service方法要跟着变;而Application这种方式不需要变化,只要在Inventory对象内部计算就可以了。代码放到了合适的地方,计算在合适层次,一切都很合理。这种设计可以充分利用各种OOD、OOP的理论把业务逻辑实现的很漂亮。

  • 充血模型的缺点
    从上面的例子,在Repository的load 到执行业务方法,再到save回去,这是需要耗费一定时间的,但是这个过程中如果多个线程同时请求对Inventory库存的锁定,那就会导致状态的不一致,麻烦的是针对库存的并发不仅难处理而且很常见。
    贫血模型完全依靠数据库对并发的支撑,实现可以简化很多,但充血模型就得自己实现了,不管是在内存中通过锁对象,还是使用Redis的远程锁机制,都比贫血模型复杂而且可靠性下降,这是充血模型带来的挑战。更好的办法是可以通过事件驱动的架构来取消并发。

领域模型和微服务的关系

上面讲了领域模型的实现,但是他和微服务是什么关系呢?在实践中,这个Inventory是一个限界上下文的聚合根,我们可以认为一个限界上下文是一个微服务进程。
不过问题又来了,一个库存的Inventory一定和商品信息是有关联的,仅仅靠Inventory中的冗余那点商品ID是不够的,商品的上下架状态等等都是业务逻辑需要的,那不是又把商品Sku这样的重型对象引入了这个微服务?两个重型的对象在一个服务中?这样的微服务拆不开啊,还是必须依靠商品库?!
请参考下一篇,通过事件驱动架构来完成领域间的松耦合。

版权说明

本文采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。

关注我

微信公众号
qrcode_for_8

@JoeCao JoeCao changed the title 多研究些架构,少谈些框架(2)-- 充血模型 多研究些架构,少谈些框架(2)-- 微服务和充血模型 Jun 12, 2017
@xingzheone
Copy link

一家之言: 面向对象这个思想也许是一个错误的思想.... 微服务 抛开面向对象思想, 面向数据

@JoeCao
Copy link
Owner Author

JoeCao commented Jun 14, 2017

@XingZheFeng 你的担忧很有道理,我在文章里面也说了,这个世界是纯数学构建的,有一天我们可以用公式来精确的计算任何一个逻辑。就像围棋,我们把每个变化都算清楚就是围棋之神了, 但是现在我们还做不到,只能通过剪枝和蒙特卡洛加上深度学习去寻求近似解。 而面向对象、DDD本身是经验性的,不能严格证明。但是他是我看到的目前最接近业务逻辑近似解的方案了。

@yingzidd12
Copy link

文章写的不错,求私聊

@yingzidd12
Copy link

我是电子工业出版社博文视点策划编辑,微信yingzidd

@JoeCao
Copy link
Owner Author

JoeCao commented Jun 19, 2017

@yingzidd12 本文采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。 有什么问题可以邮件联系我。

@magichan
Copy link

划分业务的同时,也将存储模型根据业务划分,与业务逻辑构成一个 Bound Context,引入一个问题,这样的微服务是否具有良好的水平扩展性。
毕竟微服务发展的一个驱动力是面对突发流量时的,快速反应能力。当一个微服务成为瓶颈的时候,我可以利用容器技术创建多个相同微服务。那么当存储与业务逻辑绑定的时候,这种水平扩展是如何实现。

@JoeCao
Copy link
Owner Author

JoeCao commented Aug 10, 2017

@magichan 实际上除非是图形处理这种消耗CPU的业务,我们90%以上的业务的性能瓶颈都在IO上,也就是存储上。这种情况下,你用容器创建更多的业务逻辑的微服务进程也是徒劳,并不能解决性能问题。但是如果BC和存储在一起,你反而可以有针对性的发现有性能瓶颈的BC,然后采用分区分片的方式,或者直接用NoSQL去存储热点的数据。不需要所有的数据库都跟着一起变化,充分发挥微服务独立松耦合的特性,去解决水平扩展。

@soulmz
Copy link

soulmz commented Sep 6, 2017

赞,已关注已star

@sulong
Copy link

sulong commented Sep 12, 2017

很好的文章。我觉得“充血”模型这种说法是不合适的,模型本来就应该有合适的行为,贫不好,充也不好。

@hc24
Copy link

hc24 commented Sep 18, 2017

看到文中提到的“充血”例子,不可以通过Application中通过加数据库事务实现?忘指教,已经开发了N年的贫血了。

@JoeCao
Copy link
Owner Author

JoeCao commented Sep 19, 2017

@hc24 充血模型已经把状态加载到内存中了,修改都是在自己的内存里面,再写到数据库的时候,必然会有并发的问题。这个缺陷也是充血模型必须付出的代价,任何技术都是有代价的。所以一般简单的基础业务,没有必要使用充血模型。

@thefloe
Copy link

thefloe commented Nov 6, 2017

充血模型不等于一定要把状态加载到内存中去操作,重点在于把业务规则封装到对象中
非要加载到内存中去操作,是人为制造问题然后又来提解决方案
做DDD/MSA应该保持无状态,整体架构是很简单清晰的

@ghost
Copy link

ghost commented Apr 27, 2018

【虽然我们用Java这样的面向对象语言来开发,但是其实和过程型语言是一样的,所以很多情况下大家用数据库的存储过程来替代Java写逻辑反而效果会更好】
个人拙见:互联网企业其实是比较忌讳使用存储过程来处理业务,因为大多数高并发场景都是为了解决数据库的负担,使用存储过程有些适得其反。

@fqdeng
Copy link

fqdeng commented Sep 19, 2018

@thefloe 貌似值对象 集合的话 会全部加载进来吧

@garfeildma
Copy link

贫血模型还有个很大的问题是可测试性太差

@galaio
Copy link

galaio commented Apr 27, 2020

充血模型不等于一定要把状态加载到内存中去操作,重点在于把业务规则封装到对象中
非要加载到内存中去操作,是人为制造问题然后又来提解决方案
做DDD/MSA应该保持无状态,整体架构是很简单清晰的

同意,在于行为抽象,具体持久化和防并发完全可以依靠于db。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests