最近在工作中实践DDD(Domain Driven Design)的时候,对资源库该如何设计思考了很久,对于可能存在的并发竞争问题,最终选择了乐观锁这一方案,在这里记录一下原因。
DDD中的资源库
首先来说一下什么是资源库,在DDD中,每个领域的逻辑是非常重要的,所以它应该被包裹起来,不做任何与业务逻辑不相关的事,而这其中与数据库的交互就是与业务逻辑丝毫不相关的,而资源库就是负责把领域层和持久化层隔离开的一个类似防腐层的玩意。
- 举一个具体的例子:
- 有这样一个支付域,支付肯定是它的核心域,那么支付域就不该操作任何持久化方面的东西,而是应该把持久化交给资源库层去做
ps:支付域中也不应该直接调用资源库提供的接口,而是外界还有一个Service层去调用,这个与今天的主题没多大关系,就不细说了
资源库中的并发问题
在并发场景中,我们常常会遇到竞争问题,而Postgresql的事务级别默认是read committed, 就拿刚才支付场景来说,可能存在以下情况:
sequenceDiagram
事务A(钱包余额500元)->>事务A(钱包余额300元): 支付200元(update money=500-200)
事务B(钱包余额500元)->>事务B(钱包余额500元): 获取到余额500元
事务A(钱包余额300元)->>事务A(钱包余额300元): 提交事务
事务B(钱包余额500元)->>事务B(钱包余额700元): 收款200元((update money=500+200)
事务B(钱包余额700元)->>事务B(钱包余额700元): 提交事务
可以看到,第一次的扣款消失了(还有这种好事?),这就是read committed隔离级别可能会造成的丢失修改问题。
丢失修改: 两个事务同时更新一行数据,一个事务对数据的更新把另一个事务对数据的更新覆盖了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。
如何解决丢失修改问题?
提高数据库隔离级别,改为串行,但这样会造成性能大幅下降,只有在并发量实在很小,或者没有并发量的情况下才会考虑。
悲观锁:方法执行时增加分布式锁,来控制同一账户同一时刻只有一个线程可对其进行操作。效果等同将事务级别改为串行,也是排队执行,并发性能也低,只是锁机制不是由数据库实现了而已。
乐观锁:增加一个版本的概念,每次一个事务提交修改,就将版本号加一,如果后续其他事务提交的时候发现当前更新的模型版本号改变了,就使事务失败,这样大大提高性能, 但就是可能需要事务重试或自旋
最终选择乐观锁的原因
乐观锁适用于读多写少的场景,若是写很多的话,必然会造成很多冲突,从而要让很多事务重试,所以在写多的场景,悲观锁还是更合适,但由于我的业务场景基本上都是读取,所以乐观锁非常适用。
额外思考
由于乐观锁比较的只有在此次事务中更新过的模型版本号,而也有可能修改是基于旧数据的,这个问题怎么解决呢?
这就需要讲到DDD所带来的好处了,DDD规定聚合之间是只需要最终一致性的,所以识别出不同的聚合后,聚合A的修改并不需要实时同步到聚合B,这也就使上述问题(即暂时的数据不一致)能够容忍了
前提是要识别出正确的聚合与一致性边界!