乐观锁
为什么需要采用乐观锁?
由于activiti一个周期的transaction时间可能比较长,且同一流程实例中存在任务并发执行等场景。设计者将update、insert、delete事务性的操作推迟至command结束时完成,这样尽量降低锁冲突的概率,由此产生基于mybatis上封装的session cache来管理这些中间状态的实体对象。但在充分竞争情况下锁是不可避免的,进一步利用乐观锁机制能保证执行模型的一致性。因为往往如果锁定相同数据记录说明多个相互影响的command并发执行,安全的策略就是让第一个命令成功,其他皆失败。
PS:
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自 外部系统的事务处理)修改持保守态度,其它事务会一直阻塞,直到这个事务结束。因此,在整个数据处理过程中,将数据处于锁定 状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能 真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系 统不会修改数据)。
对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制,不会锁住任何东西。大多是基于数据版本 ( Version )记录机制实现,更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据予以驳回。
死锁
在一个command执行过程中存在开始独立的新事务PROPAGATION_REQUIRES_NEW,例如IdGenerator,DecrementJobRetriesCmd。
1、IdGenerator:为什么不用数据库序列生成器?因为此前所说我们的数据库操作全部延迟到最后执行,故而无法使用数据库的自增序列,而是采用外部序列来联系各实体。
2、DecrementJobRetriesCmd:asynchronous job执行失败,在TransactionState.ROLLED_BACK时执行job异常登记钩子
在command中嵌入开启新subcommand(该subcommand包含需获得数据库连接资源),然而在有限资源的情况下自身已经持有资源再试图去获取同类资源是相当危险的方案,由此引发的竞争往往会将你推入死锁的深渊。正如下图所示便是activiti在高并发场景下产生死锁的原因。
exclusive jobs
问题抛出
并行execution中servicetask async=true,当他们各自被不同worker thread执行,当一个execution跑到join节点会判断是否除己之外所有的income flow都已到达,如果是则走过join否则join等待。但是当多个flow同时到达join,因为此时他们各自对于对方是不可见的,都假定对方没有到达,导致都认为要等待对方,从而导致join永远不会执行。
前面所述乐观锁在并发情况下同时只会保证第一个提交的job成功,其他抛出ActivitiOptimisticLockingException失败,从而在一定的时间后重试,但重试的次数是有限的(默认为3次),并行线路越多冲突重试的可能性也越大。且如果task service不在bpm transaction控制之下(比如POST外部系统接口),则业务不能正确回滚,被执行多次。
exclusive job 保证隶属同一process instance的job是被顺序执行,即在org.activiti.engine.impl.jobexecutor.AcquireJobsRunnable中将同一流程的job压人同一批次。