知识卡片

粗粒度锁的两种实现:共享锁与根对象锁

结构图卡

内容

一组对象经常被当作一个整体来修改(比如顾客和他的一组地址),逐个加锁很麻烦——不仅调用方要自己写代码找齐所有对象才能加锁,乐观离线锁下批量读取多个对象来加锁会拖累性能,悲观离线锁下则会让锁表争夺更激烈。粗粒度锁用一把锁覆盖一整组对象来解决这个问题,关键是先为这组对象建一个”控制点”。共享锁:让组内每个对象共享同一个版本号(不是”版本相同”,而是”共用同一个版本号变量”),任何一个成员被修改,这个共享版本号递增一次,就相当于给整组对象上了锁;配合悲观离线锁时,同样可以让组内成员共享某种锁标记(一个共享的版本对象就很适合充当这个标记)。根对象锁:把一簇相关对象看作一个”聚集”(这是Eric Evans与David Siegel提出的概念)——聚集有一个唯一的根对象作为访问入口,以及定义”聚集范围”的边界,锁住这个根对象,按定义就等于锁住了整个聚集里的所有成员;要让这招管用,聚集里的每个成员都得有办法导航到根对象(可以直接持有到根的引用,也可以逐级向上访问父节点直到抵达根,层级深的话后者会拖累性能,因此导航中间对象时应该配合延迟加载——但要小心这种延迟加载如果跨越多个系统事务,可能导致系统状态不一致,这是应当避免的)。两种实现各有代价:共享锁在关系数据库里意味着几乎所有查询都要联接版本表;根对象锁则要在加锁前导航读取一批中间对象,也拖累性能,而且和悲观离线锁搭配时经常需要重新读取这些中间对象以确保它们是最新的。使用粗粒度锁最根本的理由应该是业务需求本身——比如一个带着若干资产的租约对象,允许一个用户改租约的同时另一个用户改其中的资产,在业务上根本说不通,这才是”必须把租约和资产绑成一把锁”的真正原因,而不是单纯出于技术层面的性能考量;同时也要警惕不要为了套用这个模式而生造出不自然的对象关系。

结构图

flowchart TB
  A["粗粒度锁两种实现"]
  A --> B["共享锁<br/>组内成员共享同一版本号<br/>关系数据库需联接版本表"]
  A --> C["根对象锁(基于聚集)<br/>锁根即锁全部成员<br/>需要导航路径+延迟加载"]
  C --> D["风险:跨系统事务的延迟加载<br/>可能导致状态不一致"]

参考来源

- 位置:《企业应用架构模式》第二部分"模式"之"第16章 离线并发模式"之"16.3 粗粒度锁"(源文件:_epub-src/OEBPS/Text/000196.html、000197.html) - 结论依据:原文说明"用乐观离线锁让组中的每个对象共享同一个版本号来建立一个控制点……Eric Evans和David Siegel把一簇相关对象看成一个聚集……锁住根对象就锁住了聚集中所有的对象……使用粗粒度锁最明显的理由是为了满足业务需要……在使用粗粒度锁时,警惕不要创建一些不自然的对象关系",直接支撑本卡结构图。 - 原始内容:考虑一个拥有一些资产的租约对象。在一个用户修改租约的同时允许其他用户修改资产,在业务上没有任何意义。锁住租约或资产中的任何一个时都应该将租约和资产同时锁住。