专栏首页SpringCloud专栏关于处理某一个事件需要关联多个事件或表的情况下,一些思考

关于处理某一个事件需要关联多个事件或表的情况下,一些思考

这个场景是非常常见,毕竟纯粹的单表的CRUD比较少,大部分时候都是操作了某个表、某个业务,然后需要多个表进行更改。

譬如社交信息流类的,我发了一篇帖子,首先UserPost表需要添加一条数据,然后可能需要给关注我的人的信息流里也插一条数据,再做一些推送类的事件等等可能要很多步骤。

像电商类的下单之类的操作关联的表就更多了。

这里必然会涉及的问题就是业务代码耦合,总不能我添加了一篇帖子,然后就在帖子保存之后,再去操作N个其他的表。首先我们想到的就是业务的单一职责,譬如PostService里,就只能操作Post的增删改查,而不能再去做其他表的操作,里面也最好不要出现别的业务类的Service调用。

说到这里,就得提一下之前的一篇文章,贫血,充血模型的解释以及一些经验(http://blog.csdn.net/tianyaleixiaowu/article/details/75416209)。这个文章介绍的模型是比较有意义的。大概是说,传统的Controller,Service,Dao这种单薄的3层结构是不合适的,尤其是Service会不可避免的处理N多表的逻辑,会产生大量的重复代码。他的解决方案是将每个表做一个单薄的Manager管理类,只处理自己表的CRUD。然后对于要处理多个表的业务逻辑,再去定义一个相应的Service,在这个Service里去调用各个单表的Manager。在Controller里,应根据需要来使用Manager或者Service。

需要注意,如果你无法界定单表的界限,就是那种类里也关联了别的类,请将类里关联的类改成被关联类的Id,而不是去定义这个对象。这一点尤其是对使用hibernate来说,尽量不要去定义一个类关联,而是使用对方的Id,并为Id加上索引。而且尽量避免使用外键,请参考阿里巴巴Java手册。当项目变大,你会被外键搞的崩溃。不要贪图级联查询时的方便,来为项目变大后的巨大麻烦买单。

回归正题,怎么去做在处理某一个事件时,还需要处理N多别的事件,而又不让代码耦合进来。

说四种方案:

1.采用Spring的接口注解功能

spring有一个功能是,你在Autowired一个接口集合时,它会自动把该接口的实现类都注入进来。

譬如我要保存一个Post,那么我定义一个PostAddCallBack接口,里面有个方法void postAdd(Post post)即可。

然后所有需要监听Post新增的业务类都去实现PostAddCallBack接口即可,并在接口方法里做自己的业务。将来不需要监听了,就删除实现该接口即可,这样系统就成为了一个可插拔式的,想监听哪个事件就去实现哪个事件的接口,而不用去找该事件的触发源,不去和触发源代码耦合。

对于Post的add就像下面的写法

/**
 * Created by wuwf on 17/4/20.
 */
@Service
public class PostService{
    @Autowired
    private PostRepository postRepository;

    @Autowired
    private List<PostAddCallBack> postAddCallBackList;

    public Post findById(int id) {
        return postRepository.findById(id);
    }

    public Post save(Post post) {
        Post temp = postRepository.save(post);
        postAddCallBackList.forEach(postAddCallBack -> postAddCallBack.postAdd(temp));
        return temp;
    }

}

这样就能保持单表业务的单一性。而且便于事务管理。需要注意,如果该接口没有任何实现类,在forEach会报错。

然后可以看到,这个过程是同步的,就是你保存了一篇Post后,需要等待所有的接口实现类都做完postAdd里的事后,才会给客户端返回Post,所耗费的时间为各个方法的总和。还有一点,它是无序的,不适用于需要保持不同的实现类按特定顺序执行方法的地方。

而且这种方式仅适合于单体应用,如果事件需要被别的工程监听,那自然是用不了这接口了,就需要借助于消息队列。

2.使用Spring的ApplicationEvent事件

spring的ApplicationEvent同样支持订阅、发布功能,而且可以定义顺序,还可以定义是否异步执行,能够弥补上面的方式的一些不足,适用于对性能要求高,事务要求不高的场合。

使用也很简单,我们需要定义一个事件,用来装载要传递的实体对象,我这里简单写个String测试。

import org.springframework.context.ApplicationEvent;


public class PostAddEvent extends ApplicationEvent {

    public PostAddEvent(String post) {
        super(post);
    }
}

然后在需要发布事件的地方这样写

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class PostService {
    @Autowired
    private ApplicationEventPublisher publisher;

    public void  add() {
        System.out.println("新建了一个post");
        //发布事件
        publisher.publishEvent(new PostAddEvent("post发布了"));
        System.out.println(Thread.currentThread().getName());
    }
}

调用publicEvent即可,将对象放到Event里。

接收的地方这样写

import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class TestService {

    @EventListener
    public void listen(PostAddEvent postAddEvent) {
        System.out.println("收到事件" + postAddEvent.getSource());
        System.out.println(Thread.currentThread().getName());
    }
}

用@EventListener注解即可,系统会根据参数来确定哪些事件会发送到该方法上。这样的监听者可以定义多个,系统会按照随机顺序将事件发送到所有的监听者。

如果某个处理非常耗时,我们可以使用异步方式来处理。在启动类加上@EnableAsync注解,方法上加@Async注解即可。我代码里有打印当前线程名字的地方,可以看到不加async时线程名相同,加了后就不同了。

@Service
public class TestService {

    @EventListener
    @Async
    public void listen(PostAddEvent postAddEvent) {
        System.out.println("收到事件" + postAddEvent.getSource());
        System.out.println(Thread.currentThread().getName());
    }
}

Spring在启动过程中的事件也是通过该方式发送的,譬如之前我们做过类似于监听Spring启动完毕后去做初始化Mongo之类的操作。就是这样的写法

public class SmartTalkApplicationListener implements ApplicationListener {
    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        if (applicationEvent instanceof ContextRefreshedEvent) {
            ApplicationContext applicationContext = ((ContextRefreshedEvent) applicationEvent).getApplicationContext();
            MongoTemplate mongoTemplate = null;
            try {
                mongoTemplate = (MongoTemplate) applicationContext.getBean("mongoTemplate");
                mongoTemplate.setReadPreference(ReadPreference.secondaryPreferred());
            } catch (BeansException e) {

            }
            new SpringContextUtil().setApplicationContext(applicationContext);
        }
    }
}

可以看到,Spring发出的event叫ContextRefreshEvent,当然不止这一个,还有很多个Spring状态的事件。上面的写法是实现ApplicationListener接口,使用@EventListener注解会更方便一些。 以上是同步和异步两种方式,都是无序的,如果需要有序化的事件,则需要在方法再添加一个@Order注解

@Service
public class TestService {

    @EventListener
    @Order(1)
    public void listen(PostAddEvent postAddEvent) {
        System.out.println("test" + postAddEvent.getSource());
    }
}

如果有多个方法监听该事件,则会按照Order从小到大依次执行。不用注解的话是通过实现SmartApplicationListener接口,这里就不讲了,用注解会更方便。 而且,你可以在任何一个地方去修改事件里对象的值,修改后的值会被带到下一个order中。这种带order顺序执行的,可以用来做流程审批之类的逻辑。

3.采用消息队列

消息队列一般有点对点模式、发布订阅模式,譬如阿里的ons,我们可以采用订阅模式来完成需求。

订阅模式就是有多个客户端订阅某个事件,当事件被触发后,每个客户端都能接收到该事件。

很明显消息队列适合于完成分布式环境下的消息订阅,可以在多个不同的项目间进行事件共享,问题也很明显,就是分布式事务。至于分布式事务,就是另外的事了,比较麻烦,如果不是强实时性业务,考虑使用最终一致性即可。

4.采用Disruptor

上面的第二第三种都是典型的生产-消费者模型,即发布订阅模型,在java中ArrayBlockQueue也能够完成类似的发布订阅。

但是需要注意的是,这几个都是无法处理消费者顺序问题的!

生产者发布了事件,消费者同时接收到事件并开始处理,托若我们需求的是类似于下图这样的

生产者发布了P1,后面的都是消费者,需要C1A和C2A同时执行,C1A执行后才能执行C1B,C2A执行完后才能执行C2B,C1B和C2B都执行完了才能执行C3.

消费者既可以并行处理,也可以相互依赖形成处理的先后次序,在多线程消费者的情况下,要完成这样的功能可不容易。少不得就得各种线程锁、wait之类的。

还好有Disruptor,这个框架是一个超高性能的生产、消费者框架,能够轻松完成上面的菱形功能。细节后面的文章会有。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • SpringBoot 2.0中SpringWebContext 找不到无法使用的问题解决

    为了应对在SpringBoot中的高并发及优化访问速度,我们一般会把页面上的数据查询出来,然后放到redis中进行缓存。减少数据库的压力。

    做全栈攻城狮
  • Spring Boot Actuator详解与深入应用(三):Prometheus+Grafana应用监控

    本文系《Spring Boot Actuator详解与深入应用》中的第三篇。在前两篇文章,我们主要讲了Spring Boot Actuator 1.x与 2.x...

    aoho求索
  • spring cloud/spring boot同时支持http和https访问

           关于spring boot同时支持http和https访问,在spring boot官网73.9已经有说明文档了,同样在github上也有官网的例...

    lyb-geek
  • Spring Boot 终极清单

    我上学那会主要学的是 Java 和 .Net 两种语言,当时对于语言分类这事儿没什么概念,恰好在2009年毕业那会阴差阳错的先找到了 .Net 的工作,此后就开...

    王磊的博客
  • AOP 那点事儿 ( 续集 )

    在上篇中,我们从写死代码,到使用代理;从编程式 Spring AOP 到声明式 Spring AOP。一切都朝着简单实用主义的方向在发展。沿着 Spring A...

    Java高级架构
  • 教你理清SpringBoot与SpringMVC的关系

    spring boot就是一个大框架里面包含了许许多多的东西,其中spring就是最核心的内容之一,当然就包含spring mvc。spring mvc 是只是...

    Java团长
  • Spring事务你可能不知道的事儿

    关于事务,简单来说,就是为了保证数据完整性而存在的一种工具,其主要有四大特性:原子性,一致性,隔离性和持久性。对于Spring事务,其最终还是在数据库层面实现的...

    黄泽杰
  • 推荐几个对Asp.Net开发者比较实用的工具 2

    推荐几个对Asp.Net开发者比较实用的工具。大家有相关工具也可以在评论区留言,一起努力学习。

    做全栈攻城狮
  • 公司ES升级带来的坑怎么填?

    公司的ES最近需要全部进行升级,目的是方便维护和统一管理。以前的版本不统一,这次准备统一升级到一个固定的版本。

    猿天地
  • 到底每天要写多少行代码,才能成为大牛

    首先需要普及一个常识:并不是写的代码越多,就离成为大牛越近。成为大牛和成为胖子是完全不同的,吃得越多,越快成为胖子;代码写的越多,越快成为大牛?这两个命题之间不...

    三哥

扫码关注云+社区

领取腾讯云代金券