范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

事件驱动模型实践

  前言
  Spring的框架中还是在日常MVC代码的编写过程中,巧用事件驱动模型都能很好的提高代码的可维护性。
  因此,本文将对DDD中使用事件驱动模型建立与踩坑做一个系统性的介绍。从应用层面出发,帮助大家更好的去进行架构迁移。
  事件驱动模型
  为什么需要事件驱动模型
  一个框架,一门技术,使用之前首先要清楚,什么样的业务场景需要使用这个东西。为什么要用跟怎么样把他用好更加重要。
  假设我们现在有一个比较庞大的单体服务的订单系统,有下面一个业务需求:创建订单后,需要下发优惠券,给用户增长积分
  先看一下,大多数同学在单体服务内的写法。 //在orderService内部定义一个放下 @Transactional(rollbackFor = Exception.class) public void createOrder(CreateOrderCommand command){   //创建订单   Long orderId = this.doCreate(command);   //发送优惠券   couponService.sendCoupon(command,orderId);   //增长积分   integralService.increase(command.getUserId,orderId); }
  上面这样的代码在线上运行会不会有问题?不会!
  那为什么要改呢?
  原因是,业务需求在不断迭代的过程中,与当前业务非强相关的主流程业务,随时都有可能被替换或者升级。
  双11大促,用户下单的同时需要给每个用户赠送几个小礼品,那你又要写一个函数了,拼接在主方法的后面。双11结束,这段要代码要被注释。有一年大促,赠送的东西改变,代码又要加回来…
  来来回回的,订单逻辑变得又臭又长,注释的代码逻辑很多还不好阅读与理解。
  如果用了事件驱动模型,那么当第一步创建订单成功之后,发布一个创建订单成功的领域事件。优惠券服务,积分服务,赠送礼品等等监听这个事件,对监听到的事件作出相应的处理。
  事件驱动模型代码 //在orderService内部定义一个放下 @Transactional(rollbackFor = Exception.class) public void createOrder(CreateOrderCommand command){   //创建订单   Long orderId = this.doCreate(command);   publish(orderCreateEvent); }  //各个需要监听的服务 public void handlerEvent(OrderCreateEvent event){ //逻辑处理 }
  代码解耦,高度符合开闭原则 事件驱动模型选型
  spring中的事件驱动机制
  spring在4.2之后提供了@EventListener注解,让我们更便捷的使用监听。
  了解过spring启动流程的同学都知道,Spring容器刷新的时候会发布ContextRefreshedEvent事件,因此若我们需要监听此事件,直接写个监听类即可。 @Slf4j @Component public class ApplicationRefreshedEventListener implements   ApplicationListener {      @Override     public void onApplicationEvent(ContextRefreshedEvent event) {         //解析这个事件,做你想做的事,嘿嘿     } }
  同样的我们也可以自己来定义一个事件,通过ApplicationEventPublisher发送。  //领域事件基类  @data  @NoArgsConstructor public abstract class BaseDomainEvent implements Serializable {       //领域事件id      private String demandId;      //发生时间      private LocalDateTime occurredOn;      //领域事件数据      private T data;      public BaseDomainEvent(String demandId, T data) {         this.demandId = demandId;         this.data = data;         this.occurredOn = LocalDateTime.now();     } }
  定义统一的业务总线发送事件 //领域事件发布接口  public interface DomainEventPublisher {      /*      * 发布事件      *      * @param event 领域事件      */      void publishEvent(BaseDomainEvent event);  }//领域事件发布实现类 @Component @Slf4j public class DomainEventPublisherImpl implements DomainEventPublisher {      @Autowired     private ApplicationEventPublisher applicationEventPublisher;      @Override     public void publishEvent(BaseDomainEvent event) {         log.debug("发布事件,event:{}", event.toString());         applicationEventPublisher.publishEvent(event);     } }
  监听事件 @Component @Slf4j public class UserEventHandler {      @EventListener     public void handleEvent(DomainEvent event) {        //doSomething     } }
  事件驱动之事务管理
  平时我们在完成某些数据的入库后,发布了一个事件。后续我们进行操作记录在es的记载,但是这时es可能集群响应超时了,操作记录入库失败报错。但是从业务逻辑上来看,操作记录的入库失败,不应该影响到主流程的逻辑执行,需要事务独立。亦或是,如果主流程执行出错了,那么我们需要触发一个事件,发送钉钉消息到群里进行线上业务监控,需要在主方法逻辑中抛出异常再调用此事件。这时,我们如果使用的是@EventListener,上述业务场景的实现就是比较麻烦的逻辑了。
  为了解决上述问题,Spring为我们提供了两种方式:
  (1) @TransactionalEventListener注解。
  (2) 事务同步管理器TransactionSynchronizationManager。
  本文针对@TransactionalEventListener进行一下解析。
  我们可以从命名上直接看出,它就是个EventListener,在Spring4.2+,有一种叫做@TransactionEventListener的方式,能够实现在控制事务的同时,完成对对事件的处理。 //被@EventListener标注,表示它能够监听事件 @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented@EventListener public @interface TransactionalEventListener {    //表示当前事件跟随消息发送方事务的出发时机,默认为消息发送方事务提交之后才进行处理。   TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;     //true时不论发送方是否存在事务均出发当前事件处理逻辑    boolean fallbackExecution() default false;     //监听的事件具体类型,还是建议指定一下,避免监听到子类的一些情况出现    @AliasFor(annotation = EventListener.class, attribute = "classes")    Class<?>[] value() default {};     //指向@EventListener对应的值@AliasFor(annotation = EventListener.class, attribute = "classes")    Class<?>[] classes() default {};     //指向@EventListener对应的值    String condition() default "";  } public enum TransactionPhase {    // 指定目标方法在事务commit之前执行    BEFORE_COMMIT,     // 指定目标方法在事务commit之后执行     AFTER_COMMIT,      // 指定目标方法在事务rollback之后执行     AFTER_ROLLBACK,     // 指定目标方法在事务完成时执行,这里的完成是指无论事务是成功提交还是事务回滚了    AFTER_COMPLETION   }
  我们知道,Spring的事件监听机制(发布订阅模型)实际上并不是异步的(默认情况下),而是同步的来将代码进行解耦。而@TransactionEventListener仍是通过这种方式,但是加入了回调的方式来解决,这样就能够在事务进行Commited,Rollback…等时候才去进行Event的处理,来达到事务同步的目的。 实践及踩坑
  针对是事件驱动模型里面的@TransactionEventListener与@EventListener假设两个业务场景。
  新增用户,关联角色,增加关联角色赋权操作记录。 统一事务:上述三个操作事务一体,无论哪个发生异常,数据统一回滚。 独立事务:上述三个操作事务独立,事件一旦发布,后续发生任意异常均不影响。
  统一事务
  用户新增 @Service @Slf4j public class UserServiceImpl implements UserService {      @Autowired     DomainEventPublisher domainEventPublisher;      @Transactional(rollbackFor = Exception.class)     public void createUser(){         //省略非关键代码save(user);         domainEventPublisher.publishEvent(userEvent);     } }
  用户角色关联 @Component @Slf4j public class UserEventHandler {      @Autowired     DomainEventPublisher domainEventPublisher;      @Autowired     UserRoleService userRoleService;      @EventListener     public void handleEvent(UserEvent event) {         log.info("接受到用户新增事件:"+event.toString());         //省略部分数据组装与解析逻辑         userRoleService.save(userRole);         domainEventPublisher.publishEvent(userRoleEvent);     }  }
  用户角色操作记录 @Component @Slf4j public class UserRoleEventHandler {      @Autowired     UserRoleRecordService userRoleRecordService;      @EventListener     public void handleEvent(UserRoleEvent event) {         log.info("接受到userRole事件:"+event.toString());         //省略部分数据组装与解析逻辑         userRoleRecordService.save(record);     }  }
  以上即为同一事务下的一个逻辑,任意方法内抛出异常,所有数据的插入逻辑都会回滚。
  给出一下结论,@EventListener标注的方法是被加入在当前事务的执行逻辑里面的,与主方法事务一体。
  踩坑1
  严格意义上来说这里不算是把主逻辑从业务中拆分出来了,还是在同步的事务中,当然这个也是有适配场景的,大家为了代码简洁性与函数级逻辑清晰可以这么做。但是这样做其实不是那么DDD,DDD中应用服务的一个方法即为一个用例,里面贯穿了主流程的逻辑,既然是当前系统内强一致性的业务,那就应该在一个应用服务中体现。当然这个是属于业务边界的。举例的场景来看,用户与赋权显然不是强一致性的操作,赋权失败,不应该影响我新增用户,所以这个场景下做DDD改造,不建议使用统一事务。
  踩坑2 @Component @Slf4j public class UserEventHandler {      @Autowired     DomainEventPublisher domainEventPublisher;      @Autowired     UserRoleService userRoleService;      @EventListener     @Async     public void handleEvent(UserEvent event) {         log.info("接受到用户新增事件:"+event.toString());         //省略部分数据组装与解析逻辑         userRoleService.save(userRole);         domainEventPublisher.publishEvent(userRoleEvent);         throw new RuntimeException("制造一下异常");     }  }
  发现,用户新增了,用户角色关联关系新增了,但是操作记录没有新增。第一个结果好理解,第二个结果就奇怪了把,事件监听里面抛了异常,但是居然数据保存成功了。
  这里其实是因为UserEventHandler的handleEvent方法外层为嵌套@Transactional,userRoleService.save操作结束,事务就提交了,后续的抛异常也不影响。为了保持事务一致,在方法上加一个@Transactional即可。
  独立事务
  @EventListener作为驱动加载业务分散代码管理还挺好的。但是在DDD层面,事务数据被杂糅在一起,除了问题一层层找也麻烦,而且数据捆绑较多,还是比较建议使用@TransactionalEventListener
  用户新增 @Service@Slf4j public class UserServiceImpl implements UserService {      @Autowired     DomainEventPublisher domainEventPublisher;      @Transactional(rollbackFor = Exception.class)     public void createUser(){         //省略非关键代码save(user);         domainEventPublisher.publishEvent(userEvent);     } }
  用户角色关联 @Component@Slf4j public class UserEventHandler {      @Autowired     DomainEventPublisher domainEventPublisher;      @Autowired     UserRoleService userRoleService;      @TransactionalEventListener     public void handleEvent(UserEvent event) {         log.info("接受到用户新增事件:"+event.toString());         //省略部分数据组装与解析逻辑         userRoleService.save(userRole);         domainEventPublisher.publishEvent(userRoleEvent);     } }
  用户角色操作记录 @Component @Slf4j public class UserRoleEventHandler {      @Autowired     UserRoleRecordService userRoleRecordService;      @TransactionalEventListener     public void handleEvent(UserRoleEvent event) {         log.info("接受到userRole事件:"+event.toString());         //省略部分数据组装与解析逻辑         userRoleRecordService.save(record);     }  }
  一样的代码,把注解从@EventListener更换为@TransactionalEventListener。执行之后发现了一个神奇的问题, 用户角色操作记录 数据没有入库!!! protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {    if (txInfo != null && txInfo.getTransactionStatus() != null) {       if (logger.isTraceEnabled()) {          logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");       }       //断点处       txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());    } }
  配置文件中添加以下配置 logging:level:org:mybatis: debug
  在上述代码的地方打上断点,再次执行逻辑。
  注意看接受到用户新增事件之后的日志,SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9af2818] was not registered for synchronization because synchronization is not active说明当前事件是无事务执行的逻辑。再回过头去看一下@TransactionalEventListener,默认配置是在事务提交后才进行事件执行的,但是这里事务都没有,自然也就不会触发事件了。
  那怎么解决上面的问题呢?
  其实这个东西还是比较简单的: 可以对监听此事件的逻辑无脑标注@TransactionalEventListener(fallbackExecution = true),无论事件发送方是否有事务都会触发事件。 在第二个发布事件的上面标注一个@Transactional(propagation = Propagation.REQUIRES_NEW),切记不可直接标注@Transactional,这样因为userService上事务已经提交,而@Transactional默认事务传播机制为Propagation.REQUIRED,如果当前没有事务,就新建一个事务,如果已经存在一个事务,加入到这个事务中。
  userService中的事务还存在,只是已经被提交,无法再加入,也就是会导致操作记录仍旧无法被插入。 DDD中的事件驱动应用
  理清楚spring中事件驱动模型之后,我们所要做的就是开始解耦业务逻辑。
  通过事件风暴理清楚业务用例,设计完成聚合根,划分好业务领域边界,将原先杂糅在service里面的各个逻辑根据聚合根进行: 对于聚合的每次命令操作,都至少一个领域事 件发布出去,表示操作的执行结果 每一个领域事件都将被保存到事件存储中 从资源库获取聚合时,将根据发生在聚合上的 事件来重建聚合,事件的重放顺序与其产生顺序相同 聚合快照:将聚合的某一事件发生时的状态快 照序列化存储下来。

马总化腾的女儿又爆出近照,以后谁要是娶了她就不用愁了哈哈如果娶她要20万彩礼,你愿意吗?哇哈哈哈,这腿再加50万我也愿意戴隐形眼镜时的你这兄弟真是人才,这方法真是一个字绝!这么漂亮的妹子吃大葱,不知道是什么味!哈哈哈这应该叫姐姐,还是阿全州中小学校长幼儿园园长安全教育培训活动举行视频加载中近日,全州中小学校长幼儿园园长安全教育培训活动举行。活动紧密结合2022年全国安全生产月活动主题,分期分批次对全州1800余名中小学校长幼儿园园长进行校园安全管理培训,进工视评最难就业季,浪花们该以何种姿态跃入职海?1076万人,这是教育部公布的2022届高校毕业生人数,首次突破了千万大关。加之疫情影响,这届大学生的毕业季,堪称是史上最难就业季。而与往年不同的是,今年首批00后大学生将正式步入豪门献女丢官丢人钟生(姑妄言人物)的舅舅多谊,是个慷慨善良,心直口快,乐于助人的男子汉。娶亲后氏,聪慧贤淑,生得一女二男。女婿陈仁美,中了进士后,选了陕西褒城县知县,长子名必达,当日与钟生同窗,都讲给有限的几位,懂得的人(重新编辑版)重新编辑发布,是为了与鼓励我且心怀理想的朋友们交流,也为了心中憧憬美好生活的朋友一些鼓励。用手的压力测试某些人脸皮的厚度是其次和顺带的事情,因为这样并不会给我带来快感。该从哪里切入送给三十岁还单身的你,几个扎心但实用的忠告三十几岁还单身的男生,必须要注意了,下面这几个忠告你一定要好好听一下,不然真的有可能一直单身孤独终老。素材来源于网络第一,千万不要相信那些说单身生活有多爽的毒鸡汤要知道,有些人就是村干部工资低,生活却不差,开好车住好房,哪来的钱?在农村,最不能绕过去的人也许就是村干部了,尤其是疫情发生以来,村干部把守在各个村口,想让进出村子必然要和村干部打交道,甚至有很多村民为了便宜行事,还要给值守的村干部捐献物资。随着乡四字女网红和四字软饭男的非主流爱恨情仇,雷到谁了?周末来吃个瓜放松一下。搞点离谱中带着无语和好笑的网红瓜。主角是井川里予,和她的的男友摩托。以及摩托的前女友猪姐。听起来马上要进入没什么新意的三角恋剧情了是吧?是,但不完全是。因为这土耳其发现大量稀土对我们有啥影响?一文说清我们稀土现状最近土耳其爆了个消息冲上热搜,大意是他发现了天量的稀土矿,因为现在稀土一哥是我们,这下子让精土和精美分子集体高潮一波,纷纷都说,这下子有好戏看了,弄得官媒还正式下场解释这个事情。很走进生活,步入时代探索,不断探索,是我们发展的必经之路。本次三下乡社会实践活动,岭梦社会实践队调研组负责课题的调研工作。调查搜集了解当地的信息。通过调研工作更好地了解居民生活的情况,了解时代发展的根小姨子不断撩拨姐夫,终酿悲剧王某,女,是一名会计。孙某,男,是一名网络写手。两人是夫妻关系,而且关系和睦,一直很恩爱。结婚4年多,王某由于子宫不太好,一直没能怀上孩子,即使这样,夫妻俩的感情也没有受到丝毫影响
绿营煽动网暴围杨救陈?大数据显示翻车了来源看台海台湾艺人杨丞琳日前因在大陆综艺节目中表示,早年家里经济压力大,她小时候在台湾没吃过什么海鲜吃海鲜是奢侈的,就遭到岛内绿营网暴。民进党当局岛内事务主管部门也制作图卡对杨加以泡泡玛特的泡泡终于破了出品虎嗅商业消费组作者苗正卿题图视觉中国我们像一个链条一样做事,当一个项目从上到下都被看好时,它的效率和速度会很高但当我们想要做一些新的尝试时,它可能会出现问题。泡泡玛特中国区总裁吃上降压药,就离不开了除了陪小孩上医院,我有30多年没去医院挂过号了。前两年回老家定居,看见80多岁的老娘在量血压,我也随便量了一下,高压140多,低压不到90。我没太在意,一年后,高压过了150,有时名记威少会拒绝买断如果接受的话他就不再是威少了直播吧9月13日讯近日做客一档电台节目时,名记RamonaShelburne表示她相信威少不会接受买断。威少不是那种会接受买断的人。Shelburne说道。你必须同意一次买断,这不康奈利对爱德华兹的言行感到失望向被冒犯者表达歉意直播吧9月13日讯森林狼新星爱德华兹因此前在社媒发布恐同言论而正在接受联盟审查,或将遭受停赛罚款等处罚。森林狼篮球运营总裁康奈利今日代表球队官方发文表达了对此事的态度,声明中写道我一机双杯,辅食烧水两不误,大宇二合一热水壶宝宝六个月左右就要开始添加辅食了,于是宝妈们开启了新一轮的买买买,给宝宝们准备各种辅食工具。辅食机肯定是必备的,宝宝刚吃辅食时候,主要就是吃泥糊状的食物,除了婴儿米粉,其他的菜泥果安全座椅上的小宝,忍住了眼泪,没有忍住悲伤,真委屈广东一个宝妈,分享自己带孩子的日常,开开心心的带小宝宝出去玩,结果小宝在安全座椅上,委屈的不得了,想哭又不敢哭,说又说不出来,最后还是没忍住,哭的梨花带雨非常惹人怜。宝宝心里苦说是两份汽油一个给燃油车,另一份给电动汽车充电,谁跑得远?两份相同汽油,一份给燃油车用,另一份先发电再给电动汽车充电,假设两辆汽车总质量相当,哪种车跑得更远?主要想从这个角度了解一下新能源汽车是否真的节能。先烧油驱动发电再驱动汽车,其实也iPhone14系列电池保修费涨至748元Tech星球9月12日消息,据悉,苹果iPhone14系列机型的电池保外维修价格出炉,全系四款都是748元。这也是继iPhoneX涨至519元后,iPhone新机的再次涨价。此外,这里的氢能产业发展如火如荼走进大兴国际氢能示范区8月23日,中国化工报记者跟随2022中关村论坛探访世界领先科技园区建设成就主题采访活动走进位于北京大兴的国际氢能示范区,领略发展得如火如荼的北京氢能产业的风采。日加氢量可达4。8AiFA体育砸手里!德佩交易告吹,两大豪门不接手,巴萨只能留下砸手里!德佩交易告吹,两大豪门不接手,巴萨只能留下他西甲的夏窗已经正式关闭,尽管巴萨一直都在疯狂开始进行球员的大清理,想尽办法把队伍中那些性价比极低的队员进行租借或者低价售卖,但最