领域驱动设计之SQL语句要不要写?

板桥banq 

目录

  1. 领域的定义
  2. 如何编写类?
  3. 对象如何创建
  4. SQL语句要不要写

第四章

  无论我们采取以内存中模型对象为主,还是以数据库为主,都离不开SQL语句,只是SQL份量轻重不同而已。

使用SQL最主要好处是数据库的ACID事务机制,所以,如果我们无法解决在应用服务层实现事务,甚至分布式事务问题之前,还是需要JTA结合事务中间件帮助我们解决数据可靠性的问题。

前面我们讨论了实体状态修改,状态的修改如果是在内存中进行,则是由多线程完成的,多个线程或说多个操作修改一个状态,必然引起对状态资源争夺问题,同时修改肯定是不可能的了,使用锁这又会引起性能问题。状态修改如果是使用数据库事务,同样也会引起性能瓶颈,如何在性能和事务之间平衡?

事件驱动状态改变

那么有没有两全其美,可以采取用事件替代状态,因为事件就是状态切换的意思,状态为什么会变?因为事件发生了,比如你收到短信银行卡余额为零,你就进入没钱状态了。

之前我们纠结于如何保存状态,保存状态不但有内存和数据库两处同步一致性问题,就是在内存中或是在数据库字段中,还有并发读写问题,数据库是采取ACID机制解决的,A是原子性,保证多个步骤如一个步骤,D是持久保存性,C是一致性,保证有相互关联的几处数据同时一致修改,I是隔离性,最好的隔离性是完全隔离,每个操作都完全锁住数据表字段,其他操作必须等待,实现串行化操作,好比几个人并排走到小胡同前,只能一个个串行通过,这种操作并发性很差,所以性能也很差。状态在内存中修改也存在这种并发问题,使用同步锁等堵塞操作,某个时刻只能有一个操作修改状态,性能很差。不过Java语言生态发展快,无锁非堵塞并发技术很多,可选择范围大,所以,如果你把状态放在实体对象中,以修改实体对象状态为主,修改完以后,将修改事件发给数据库再修改,这是不是类似数据库的主从架构呢?总之腾挪余地很大。

但是如果以修改数据库状态为主,再同步到内存缓存中的实体对象,那么在修改状态这个环节,你还是使用传统数据库事务机制,性能受制于数据库,如果你的应用修改等写操作不大,大部分只是读取查询操作可以这么做,当然我们这里谈的状态改变,肯定属于写操作,这里也看到,DDD是适合写操作环节,大量读操作,报表统计等可不必教条遵循DDD去建模。

写操作使得状态改变,这些写操作就是事件概念,如果我们不保存状态,只保存写操作事件,形成一个事件集合,时间序列上的事件数据库,当前状态的获得只要读取事件数据库,再重新运行这些历史事件就能获得当前状态了,这样把状态的改变其实从写操作转移到读操作,写操作中只要把事件追加到数据库或磁盘文件后面,没有修改,只有新增追加,性能会很高,而且如果排队追加,保证顺序性,等同于实现了ACID中的串行化,完全满足ACID四个特性,每次只追加一个事件本身就是原子性,一致性通过时间前后顺序保证,隔离性是串行化操作,持久性则是因为追加到磁盘文件或事件日志,都是落地在磁盘上的,这其实也是关系数据库内部事件日志的工作原理,只不过我们从数据库这个Box中取出在应用层实现了。这就是事件溯源Event Sourcing基本原理,带来问题是复杂性,这如同模块化初期带来复杂化,因为组件数量增加,微服务也带来复杂性,因为服务数量增加,对象化也带来复杂性,因为对象数量增加了。p

CAP定理

如果我们采取保存事件,而不是直接保存状态,那么我们就有可能实现分布式事务。

传统分布式事务主要是2PC,两段提交方式,主要由事务中间件完成,比如Weblogic和Oracle的卖点之一是其分布式事务中间件技术,从服务的事务最终到集群的数据库事务,都有一套严格的事务机制,比如Oracle RAC方式下,通过配置Weblogic的Grid数据源能保证Weblogic事务到Oracle数据库事务的亲和性,这些都是很微妙的微调技术,但是使用这样的JTA+XA技术问题也有,比如会和Dblink冲突等,数据库需要等待比较长的Jta方法执行过程,保持这个过程中所有参与资源的可管理性,数据库也很累,硬件要求提高了,又要用钱了。

如果我们在程序设计方面多做点事情,就可更巧妙地实现分布式事务,这需要把我们思路从事务上面提高到一个更高层次,那就是看看分布式系统的CAP定理,C代表高一致性,A代表高可用性,低延迟,高性能,P代表分区容忍性,本来一直同步的两个机器如果中间通讯断了或网路抖动,能不能各自活下去?如果有一方一旦发现网络抖动,因为它当局者迷,搞不清是网络抖动还是对方当机了,那么是否能够继续运行下去,如果继续运行下去,表示容忍这种分区断裂联系,如果发现不能与对方通讯了,就拼命重连,主要工作就是疯狂重连,自己本来干的活都停下来等重试成功再进行,这是一种无法容忍分区的表现,疯狂重连是因为它要和对方同步状态,把双方状态一致性看成最重要的,结果这种疯狂行为看似符合逻辑,最终命运不是它可以主宰的,因为它选择了CAP中的CP,所以它其实丧失了可用性,甚至会死机,这就是疯狂不断同步的后果,如果我们设置重试次数以后,直接失败,这样可用性会提高,当然问题是,两边状态不同怎么办?这个你不用焦虑了,接受最终一致性吧。

如果我们有一种机制,重试以后放弃,然后在下次对方苏醒过来,不论是网络正常还是对方机器正常,都能把失败之前的数据继续同步过去,不就是实现了最终一致性了吗?

这就是Kafka这种日志型消息系统提供的机制,说白了就是保证消息传递的原子性,保证准确同步,就是失败,只要恢复正常,可从原来断开的地方继续进行。

分布式事务

如果我们把状态的写操作事件作为消息通过Kafka这样的同步工具准确送达到对方,对方把事件作为同一套代码生成的模型对象的方法中,等于零再次播放事件,不就获得了当前状态吗?也就是说,不但自己可以通过事件播放获得当前状态,也可把事件以顺序性广播出去,其他机器播放事件也能获得当前状态,当然关键问题是,如何保证其他机器上获得的事件顺序和原机器上是一致的,通过时间排序也不行,因为不同服务器之间时钟肯定无法100%同步。这个问题解决了,分布式事务问题其实就会解决,没有通用完美方案,只有适合不同上下文的合适方案。

比如如果是转账案例,一个人账户有两个转账事件,如果两个都是入账事件,那么前后顺序对于账户余额没有影响,但是如果是一个是扣款,一个是入账,那么前后顺序就有影响了。这是一种可交换性的案例吧。

另外一个思路从时钟上入手,实际时间很难同步,使用逻辑时钟吧,或用全局ID序列产生器,产生唯一ID,再根据ID排序。

小结

说了这么多,使用SQL是为了利用其数据库的事务机制,这是一般做法,在服务中引入JTA,然后配置XA数据源,这样服务中涉及的资源包括数据库或JMS都在事务两段式照顾下,一般应用没有问题,但是应对大级别访问会加重数据库负载,这时我们提出了新的解决方案,通过事件溯源来解决分布式事务,提高了性能和吞吐量,也能实现最终一致性的柔性事务,有兴趣可了解Saga模式。