实战微博互动预测之一_问题分析 以及 分布式下的事件驱动机制(Pub与Sub模式)

实战微博互动预测之一_问题分析

2017年12月08日 13:21:04 xieyan0811 阅读数:2390

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xieyan0811/article/details/78750611

1. 天池竞赛平台

 微博互动预测是一个天池平台的竞赛,和其它平台相比,天池的数据量更大,赛题更接近实际场景。微信互动比赛算是其中比较小的,训练数据也有300多M,上百万条记录(虽然数据较多,但也是普通开发机可以处理的量级)。数据内容也比较丰富,和提供匿名纯特征拼算法的竞赛相比,它需要研究业务,发挥的空间也更大。
 天池平台的大多数比赛分为线下赛和线上赛。第一阶段线下赛和Kaggle,DC差不多,就是在本地计算,然后上传预测结果,线上评分排名(每天两次),如果第一阶段排名靠前,可进入第二阶段线上赛,线上赛使用天池平台算力和工具,海量数据,支持分布计算,但也被线上工具所限。

2. 新浪微博互动预测

 微博互动预测是前两年的赛题,现在仍开放线下赛,可以上传预测结果,计分并排名。它代表了现实中的一大类应用:数据量大,需要参赛者提取特征,数据有现实中的意义,无规律数据占多数,可多维度分析等等。赛题具体见:
https://tianchi.aliyun.com/getStart/introduction.htm?spm=5176.100066.0.0.6f2c778dQSMdqc&raceId=231574

3. 观察数据

 微博互动预测的训练集是123W条微博,预测测试集中数据转发,评论和赞的情况。
 训练数据中,字段并不多,包括:用户标记,博文标记,发博时间,转发数,评论数,赞数,博文内容。

  • 用户标记:大多数用户发文不止一条,可通过转发数,评论数,赞数预测该用户的粉丝,以及粉丝的习惯。
  • 博文标记:是微博的id,可看作索引。
  • 发博时间:可分解出工作日,节假日,时间段等属性。
  • 转发数,评论数,赞数:是预测的目标,也可用于计算用户的特征和分析其相关性。
  • 博文内容:可解析出更多特征,如分词聚类,情绪分析,是否包含链接,是否包含表情,是否包含视频,是否自动生成,是否为广告(含:天猫,淘宝,超便宜),长度,是否@谁,是否为转发#,文章分类(新闻,技术,笑话,心情…)

4. 统计数据

(1) 统计转发,评论,点赞个数(后统称反馈)

 我们既可以把它当成回归问题,也可以把它当成分类问题,如果是分类问题,则是非均衡分类,score时需要考虑分布情况。
 可见,如果把所有情况都预测成0,也能拿到一定分数。

(2) 反馈个数做图

 下面列出了转发,评论,点赞的分布图,横坐标是反馈个数(如转发数),纵坐标是该反馈出现的次数,如0次转发出现了上百万次(由于影响显示,做图截取掉了)。

(3) 反馈均数

 平均每篇获得反馈个数是,转发:3.54,评论:1.26,赞:2.22。
 可见,虽然大多数人没得到反馈,但被关注的少数人拉高了平均分。

(4) 统计用户

 训练数据中共37000多个用户,人均发文33篇,首先用把每个用户得到的转发,评论,点赞的均值加在一起,可计算出关注度,即下图中的黑线,按关注度对用户排序,下图分别显示了关注度和各种反馈之间的关系,以及分布,从中也能看到在30000多人里只有几十个人平均每篇的反馈之和超过100,且以转发为主。
 截掉了图的左侧,其中显示有15000多人,从未得到过任何反馈,占了全体用户数的0.412,所以说没人理也很正常。估计可能因为不太使用微信,只发广告,自动生成消息,或者好友太少。后面会做进一步分析。

(5) 单个用户分析

 下面是对某个用户的转发分析,他共发文733篇,其中最多的一篇被转发8949次,也因为影响显示被截掉了,其中有167篇文是0次转发,大多数分布在0-100次以内。从中还可以估计一下他的粉丝数,至少有8949人,方法是max(f,l,c)。
 可见,在粉丝多的情况下,反馈更多地取决于内容。

5. 问题分析

 再来看看比赛的评分标准,下面公式中f代表转发,c代表评论,l代表赞,p代表预测,r代表真实值;deviation是真实值与预测值的偏差,具体公式如下:

precision是准确率,根据偏差算出:

sig为符号函数,当x>0时sig(x)=1,当x<=0时,sig(x)为0。Counti为每篇的反馈总数,当Counti>100时,以按100计算。
 也就是说,当计算的偏差和在正负20%以内,则将反馈总数计入成绩。有两点需要注意:
 第一,反馈越多在评分中权重越大,比如反馈为500的文章,如果预测正确,贡献是反馈为0文章的500倍。
 第二,反馈越多偏差越大,比如实际为200次转发,预测成了500次,偏差deviation=(500-200)/(200+5)=1.63;实际为2次,预测成5次,deviation=(5-2)/(2+5)=0.43。

6. 整体分析

 从直觉上看,最强的特征应该是用户的被关注度,其次是内容,然后是时间。
 试验了一下,计算出了每个用户转发,评论,赞的均值,对于训练集中出现过的用户,直接将均值四舍五入作为预测,对没出现过的用户预测为0(整体均值)。线上得分26.49%,排名260名左右。加入一些算法后成绩反而下降了,估计可能是由于少量有规律数据和大量无规律数据混在一起,规律被湮没了,当然也有算法的选择问题。
 从不同角度看:直接可见的是文章,间接可见的是用户的特征。从已有数据可以提取到用户的发文数,各种反馈的均值,方差,关注度,估计粉丝数,以及他的粉丝对他各种文章的反馈。也可以根据不同反馈(不同的人,身边不同圈子)给用户做聚类。当某个用户个人信息不足时,取他所属类别的均值,这有点像股票分析也要先分析某支股票的股性,再分析其行为。
 初步觉得这应该是一个聚类,分类和回归结合的问题,有点像树回归。我觉得在前期,相对于分析信息内容,分析用户行为可能带来更大的信息增益。

 

 

====================================================================================================================================================================================================================================================================================================================================================================

 

 

 

浅析分布式下的事件驱动机制( PubSub 模式 )

2017/09/29 | 分类: 基础技术 | 0 条评论 | 标签: PubSub, 事件驱动, 分布式

分享到:

原文出处: 徐靖峰

上一篇文章《浅析Spring中的事件驱动机制》简单介绍了Spring对事件的支持。Event的整个生命周期,从publisher发出,经过applicationContext容器通知到EventListener,都是发生在单个Spring容器中,而在分布式场景下,有些时候一个事件的产生,可能需要被多个实例响应,本文主要介绍分布式场景下的事件驱动机制,由于使用了Redis,ActiveMQ,也可以换一个名词来理解:分布式下的发布订阅模式。

JMS规范

在日常项目开发中,我们或多或少的发现一些包一些类位于java或javax中,他们主要提供抽象类,接口,提供了一种规范,如JPA,JSR,JNDI,JTA,JMS,他们是由java指定的标准规范,一流企业做标准、二流企业做品牌、三流企业做产品,虽然有点调侃的意味,但也可以见得它的重要意义。而JMS就是java在消息服务上指定的标准

The Java Message Service (JMS) API is a messaging standard that allows application components based on the Java Platform Enterprise Edition (Java EE) to create, send, receive, and read messages. It enables distributed communication that is loosely coupled, reliable, and asynchronous.

JMS(JAVA Message Service,java消息服务)API是一个消息服务的标准或者说是规范,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。

消息中间件有非常多的实现,如ActiveMQ,RabbitMQ,RocketMQ,而他们同一遵循的接口规范,便是JMS。在下文中即将出现的ConnectionFactory,Destination,Connection,Session,MessageListener,Topic,Queue等等名词,都是JMS核心的接口,由于本文的初衷并不是讲解MQ&JMS,所以这些机制暂且跳过。

定义分布式事件需求

在上一个项目中,我们对接了外网的http接口,而安全性的保障则是交给OAuth2来完成,作为OAuth2的客户端,我们需要获取服务端返回的token,而token接口的获取次数每个月是有限制的,于是我们选择使用Redis来保存,定时刷新。由于每次发起请求时都要携带token,为了更高的性能减少一次redis io,我们在TokenService中使用了本地变量缓存token。于是形成如下的token获取机制:

这个图并不复杂,只是为了方便描述需求:首先去本地变量中加载token,若token==null,则去Redis加载,若Redis未命中(token过期了),则最终调用外部的http接口获取实时的token,同时存入redis中和本地变量中。

这个需求设计到这样一个问题:大多数情况下是单个实例中发现redis中的token为空,而它需要同时获取最新token,并通知其他的实例也去加载最新的token,这个时候事件广播就可以派上用场了。

由于token缓存在了Redis中,我们首先介绍Redis的发布订阅机制。

Redis中的Pub与Sub

redis不仅仅具备缓存的功能,它还拥有一个channel机制,我们可以使用Redis来进行发布订阅。上述的token流程我们简化一下,省略保存到redis的那一环,直接介绍如何通知其他应用刷新token。

引入依赖和配置

1

2

3

4

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

1

2

3

4

5

spring:

  redis:

    database: 0

    host: localhost

    port: 6379

定义TokenService

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@Service

public class TokenService {

    @Autowired

    StringRedisTemplate redisTemplate;

    public void getToken(String username) { // <1>

        String token = UUID.randomUUID().toString();

        //模拟http接口使用用户名和密码获取token

        System.out.println(username + " 成功获取token ..." + token);

        //发送token刷新广播

        System.out.println("广播token刷新事件 ...");

        redisTemplate.convertAndSend(RedisPubSubConfig.tokenChannel, token);

    }

    public void refreshTokenListener(String token) { // <2>

        System.out.println("接到token刷新事件,刷新 token : " + token);

    }

}

<1> 模拟获取token的方法,获取token的同时发送广播。
<2> 用于接收其他应用发送过来的广播消息。

配置RedisMessageListenerContainer

在Spring应用中Event是由Spring容器管理的,而在Redis的消息机制中,Event是由RedisMessageListenerContainer管理的。我们为token配置一个channel,用于刷新token:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

@Configuration

public class RedisPubSubConfig {

    public final static String tokenChannel = "tokenChannel";

    @Bean

    RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) {

        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();// <1>

        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);

        redisMessageListenerContainer.addMessageListener(tokenRefreshListener(), new ChannelTopic(tokenChannel)); // <2>

        return redisMessageListenerContainer;

    }

    @Autowired

    TokenService tokenService;

    MessageListener tokenRefreshListener() {

        return new MessageListener() {

            @Override

            public void onMessage(Message message, byte[] pattern) {

                byte[] bytes = message.getBody(); // <3>

                tokenService.refreshTokenListener(new String(bytes));

            }

        };

    }

}

<1> RedisMessageListenerContainer用于管理所有的redis相关的发布与订阅
<2> 为Redis容器注册特定的订阅者,在本例中使用tokenRefreshListener监听tokenChannel频道,当收到消息通知时,会自动调用onMessage方法。
<3> 使用message.getBody()可以获取消息的具体内容,在本例中即token

测试结果

同样的这个应用,我们在8080,8081,8082启动三个,在8080中,我们调用tokenService.getToken(“kirito”);(注意必须要连接到redis的同一个database)

在三个控制台中我们得到了如下的结果:

8080:

1

2

3

kirito 成功获取token ...5d4d2a48-934f-450d-8806-e6095b172286

广播token刷新事件 ...

接到token刷新事件,刷新 token : 5d4d2a48-934f-450d-8806-e6095b172286

8081:

1

接到token刷新事件,刷新 token : 5d4d2a48-934f-450d-8806-e6095b172286

8082:

1

接到token刷新事件,刷新 token : 5d4d2a48-934f-450d-8806-e6095b172286

可以发现其他系统的确收到了通知。

ActiveMQ中的Pub与Sub

Redis中的发布订阅其实在真正的企业开发中并不是很常用,如果涉及到一致性要求较高的需求,专业的消息中间件可以更好地为我们提供服务。下面介绍一下ActiveMQ如何实现发布订阅。

ActiveMQ为我们提供很好的监控页面,延时队列,消息ACK,事务,持久化等等机制,且拥有较高的吞吐量,是企业架构中不可或缺的一个重要中间件。

引入依赖

1

2

3

4

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-activemq</artifactId>

</dependency>

1

2

3

4

5

6

7

8

spring:

  activemq:

    in-memory: false # <1>

    broker-url: tcp://127.0.0.1:61616

    user: admin

    password: admin

  jms:

    pub-sub-domain: true # <2>

<1> springboot的自动配置会帮我们启动一个内存中的消息队列,引入spring-boot-starter-activemq倚赖时需要特别注意这一点,本例连接本机的ActiveMQ。
<2> springboot默认不支持PubSub模式,需要手动开启。

定义TokenService

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

@Service

public class TokenService {

    @Autowired

    JmsTemplate jmsTemplate; // <1>

    @Autowired

    Topic tokenTopic; // <3>

    public void getToken(String username) {

        String token = UUID.randomUUID().toString();

        //模拟http接口使用用户名和密码获取token

        System.out.println(username + " 成功获取token ..." + token);

        //发送token刷新广播

        System.out.println("广播token刷新事件 ...");

        try {

            Message message = new ActiveMQMessage();

            message.setStringProperty("token", token);

            jmsTemplate.convertAndSend(tokenTopic, message);// <1>

        } catch (Exception e) {

            throw new RuntimeException(e);

        }

    }

    @JmsListener(destination = ActivemqPubSubConfig.tokenTopic) // <2>

    public void refreshTokenListener(Message message) throws Exception {

        System.out.println("接到token刷新事件,刷新 token : " + message.getStringProperty("token"));

    }

}

<1> 使用模板设计模式的好处体现了出来,再前面的RedisTemplate中我们也是使用同样的template.convertAndSend()发送消息
<2> JmsListener对应于EventListener,接收来自ActiveMQ中tokenTopic的消息通知
<3> tokenTopic定义在下面的config中

配置ActiveMQ的topic

1

2

3

4

5

6

7

8

@Configuration

public class ActivemqPubSubConfig {

    public final static String tokenTopic = "tokenTopic";

    @Bean

    Topic tokenTopic(){

        return new ActiveMQTopic(ActivemqPubSubConfig.tokenTopic);

    }

}

非常简单的配置,因为ActiveMQAutoConfiguration已经帮我们做了相当多的配置,我们只需要顶一个topic即可使用ActiveMQ的功能。

查看ActiveMQ的监控端

省略了发送消息的过程,实际上可以得到和Redis PubSub一样的效果。来看一下ActiveMQ自带的监控端,在发送消息后,发生了什么变化,访问本地端口http://localhost:8161/admin ,可以看到消息被消费了。

总结

本文介绍了Redis,ActiveMQ的PubSub特性,这是我理解的分布式场景下的事件驱动的使用。事件驱动是一种思想,PubSub是一种模式,Redis,ActiveMQ是一种应用,落到实处,便可以是本文介绍的token这个小小的业务实现。但是注意,使用Redis,ActiveMQ理解事件驱动可以,但是不能等同事件驱动,事件驱动还有很多其他场景下体现,笔者功力不够,无法一一介绍,怕人误解,特此强调一下。

 

 

===========================================================================================================================================================================================================================================================================

浅析 Spring 中的事件驱动机制

2017/09/29 | 分类: 基础技术 | 0 条评论 | 标签: Spring, 事件驱动

分享到:

原文出处: 徐靖峰

今天来简单地聊聊事件驱动,其实写这篇文章挺令我挺苦恼的,因为事件驱动这个名词,我没有找到很好的定性解释,担心自己的表述有误,而说到事件驱动可能立刻联想到如此众多的概念:观察者模式,发布订阅模式,消息队列MQ,消息驱动,事件,EventSourcing…为了不产生歧义,笔者把自己所了解的这些模棱两可的概念都列了出来,再开始今天的分享。

  • 设计模式中,观察者模式可以算得上是一个非常经典的行为型设计模式,猫叫了,主人醒了,老鼠跑了,这一经典的例子,是事件驱动模型在设计层面的体现。
  • 另一模式,发布订阅模式往往被人们等同于观察者模式,但我的理解是两者唯一区别,是发布订阅模式需要有一个调度中心,而观察者模式不需要,例如观察者的列表可以直接由被观察者维护。不过两者即使被混用,互相替代,通常不影响表达。
  • MQ,中间件级别的消息队列(e.g. ActiveMQ,RabbitMQ),可以认为是发布订阅模式的一个具体体现。事件驱动->发布订阅->MQ,从抽象到具体。
  • java和spring中都拥有Event的抽象,分别代表了语言级别和三方框架级别对事件的支持。
  • EventSourcing这个概念就要关联到领域驱动设计,DDD对事件驱动也是非常地青睐,领域对象的状态完全是由事件驱动来控制,由其衍生出了CQRS架构,具体实现框架有AxonFramework。
  • Nginx可以作为高性能的应用服务器(e.g. openResty),以及Nodejs事件驱动的特性,这些也是都是事件驱动的体现。

本文涵盖的内容主要是前面4点。

Spring对Event的支持

Spring的文档对Event的支持翻译之后描述如下:

ApplicationContext通过ApplicationEvent类和ApplicationListener接口进行事件处理。 如果将实现ApplicationListener接口的bean注入到上下文中,则每次使用ApplicationContext发布ApplicationEvent时,都会通知该bean。 本质上,这是标准的观察者设计模式。

而在spring4.2之后,提供了注解式的支持,我们可以使用任意的java对象配合注解达到同样的效果,首先来看看不适用注解如何在Spring中使用事件驱动机制。

定义业务需求:用户注册后,系统需要给用户发送邮件告知用户注册成功,需要给用户初始化积分;隐含的设计需求,用户注册后,后续需求可能会添加其他操作,如再发送一条短信等等,希望程序具有扩展性,以及符合开闭原则。

如果不使用事件驱动,代码可能会像这样子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class UserService {

   

    @Autowired

    EmailService emailService;

    @Autowired

    ScoreService scoreService;

    @Autowired

    OtherService otherService;

    public void register(String name) {

        System.out.println("用户:" + name + " 已注册!");

        emailService.sendEmail(name);

        scoreService.initScore(name);

        otherService.execute(name);

    }

   

}

要说有什么毛病,其实也不算有,因为可能大多数人在开发中都会这么写,喜欢写同步代码。但这么写,实际上并不是特别的符合隐含的设计需求,假设增加更多的注册项service,我们需要修改register的方法,并且让UserService注入对应的Service。而实际上,register并不关心这些“额外”的操作,如何将这些多余的代码抽取出去呢?便可以使用Spring提供的Event机制。

定义用户注册事件

1

2

3

4

5

public class UserRegisterEvent extends ApplicationEvent{

    public UserRegisterEvent(String name) { //name即source

        super(name);

    }

}

ApplicationEvent是由Spring提供的所有Event类的基类,为了简单起见,注册事件只传递了name(可以复杂的对象,但注意要了解清楚序列化机制)。

定义用户注册服务(事件发布者)

1

2

3

4

5

6

7

8

9

10

11

12

@Service // <1>

public class UserService implements ApplicationEventPublisherAware { // <2>

    public void register(String name) {

        System.out.println("用户:" + name + " 已注册!");

        applicationEventPublisher.publishEvent(new UserRegisterEvent(name));// <3>

    }

    private ApplicationEventPublisher applicationEventPublisher; // <2>

    @Override

    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { // <2>

        this.applicationEventPublisher = applicationEventPublisher;

    }

}

<1> 服务必须交给Spring容器托管
<2> ApplicationEventPublisherAware是由Spring提供的用于为Service注入ApplicationEventPublisher事件发布器的接口,使用这个接口,我们自己的Service就拥有了发布事件的能力。
<3> 用户注册后,不再是显示调用其他的业务Service,而是发布一个用户注册事件。

定义邮件服务,积分服务,其他服务(事件订阅者)

1

2

3

4

5

6

7

@Service // <1>

public class EmailService implements ApplicationListener<UserRegisterEvent> { // <2>

    @Override

    public void onApplicationEvent(UserRegisterEvent userRegisterEvent) {

        System.out.println("邮件服务接到通知,给 " + userRegisterEvent.getSource() + " 发送邮件...");// <3>

    }

}

<1> 事件订阅者的服务同样需要托管于Spring容器
<2> ApplicationListener<E extends ApplicationEvent>接口是由Spring提供的事件订阅者必须实现的接口,我们一般把该Service关心的事件类型作为泛型传入。
<3> 处理事件,通过event.getSource()即可拿到事件的具体内容,在本例中便是用户的姓名。
其他两个Service,也同样编写,实际的业务操作仅仅是打印一句内容即可,篇幅限制,这里省略。

编写启动类

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@SpringBootApplication

@RestController

public class EventDemoApp {

    public static void main(String[] args) {

        SpringApplication.run(EventDemoApp.class, args);

    }

    @Autowired

    UserService userService;

    @RequestMapping("/register")

    public String register(){

        userService.register("kirito");

        return "success";

    }

}

当我们调用userService.register(“kirito”);方法时,控制台打印信息如下:

他们的顺序是无序的,如果需要控制顺序,需要重写order接口,这点不做介绍。其次,我们完成了用户注册和其他服务的解耦,这也是事件驱动的最大特性之一,如果需要在用户注册时完成其他操作,只需要再添加相应的事件订阅者即可。

Spring 对Event的注解支持

上述的几个接口已经非常清爽了,如果习惯使用注解,Spring也提供了,不再需要显示实现

注解式的事件发布者

1

2

3

4

5

6

7

8

9

@Service

public class UserService {

    public void register(String name) {

        System.out.println("用户:" + name + " 已注册!");

        applicationEventPublisher.publishEvent(new UserRegisterEvent(name));

    }

    @Autowired

    private ApplicationEventPublisher applicationEventPublisher;

}

Spring4.2之后,ApplicationEventPublisher自动被注入到容器中,采用Autowired即可获取。

注解式的事件订阅者

1

2

3

4

5

6

7

@Service

public class EmailService {

    @EventListener

    public void listenUserRegisterEvent(UserRegisterEvent userRegisterEvent) {

        System.out.println("邮件服务接到通知,给 " + userRegisterEvent.getSource() + " 发送邮件...");

    }

}

@EventListener注解完成了ApplicationListener<E extends ApplicationEvent>接口的使命。

更多的特性可以参考SpringFramework的文档。

Spring中事件的应用

在以往阅读Spring源码的经验中,接触了不少使用事件的地方,大概列了以下几个,加深以下印象:

  • Spring Security中使用AuthenticationEventPublisher处理用户认证成功,认证失败的消息处理。

1

2

3

4

5

public interface AuthenticationEventPublisher {

   void publishAuthenticationSuccess(Authentication authentication);

   void publishAuthenticationFailure(AuthenticationException exception,

         Authentication authentication);

}

  • Hibernate中持久化对象属性的修改是如何被框架得知的?正是采用了一系列持久化相关的事件,如DefaultSaveEventListener,DefaultUpdateEventListener,事件非常多,有兴趣可以去org.hibernate.event包下查看。
  • Spring Cloud Zuul中刷新路由信息使用到的ZuulRefreshListener

1

2

3

4

5

6

7

8

9

10

11

12

private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> {

       ...

        public void onApplicationEvent(ApplicationEvent event) {

            if(!(event instanceof ContextRefreshedEvent) && !(event instanceof RefreshScopeRefreshedEvent) && !(event instanceof RoutesRefreshedEvent)) {

                if(event instanceof HeartbeatEvent && this.heartbeatMonitor.update(((HeartbeatEvent)event).getValue())) {

                    this.zuulHandlerMapping.setDirty(true);

                }

            } else {

                this.zuulHandlerMapping.setDirty(true);

            }

        }

    }

  • Spring容器生命周期相关的一些默认Event

1

ContextRefreshedEvent,ContextStartedEvent,ContextStoppedEvent,ContextClosedEvent,RequestHandledEvent

。。。其实吧,非常多。。。

总结

本文暂时只介绍了Spring中的一些简单的事件驱动机制,相信如果之后再看到Event,Publisher,EventListener一类的单词后缀时,也能立刻和事件机制联系上了。再阅读Spring源码时,如果发现出现了某个Event,但由于不是同步调用,所以很容易被忽视,我一般习惯下意识的去寻找有没有提供默认的Listener,这样不至于漏掉一些“隐藏”的特性。下一篇文章打算聊一聊分布式场景下,事件驱动使用的注意点。

公众号刚刚创立,如果觉得文章不错,希望能分享到您的朋友圈,如果对文章有什么想法和建议,可以与我沟通。

=========================================================================================================================================================================================================================================================================

浅析 Spring 中的事件驱动机制

2017/09/29 | 分类: 基础技术 | 0 条评论 | 标签: Spring, 事件驱动

分享到:

原文出处: 徐靖峰

今天来简单地聊聊事件驱动,其实写这篇文章挺令我挺苦恼的,因为事件驱动这个名词,我没有找到很好的定性解释,担心自己的表述有误,而说到事件驱动可能立刻联想到如此众多的概念:观察者模式,发布订阅模式,消息队列MQ,消息驱动,事件,EventSourcing…为了不产生歧义,笔者把自己所了解的这些模棱两可的概念都列了出来,再开始今天的分享。

  • 设计模式中,观察者模式可以算得上是一个非常经典的行为型设计模式,猫叫了,主人醒了,老鼠跑了,这一经典的例子,是事件驱动模型在设计层面的体现。
  • 另一模式,发布订阅模式往往被人们等同于观察者模式,但我的理解是两者唯一区别,是发布订阅模式需要有一个调度中心,而观察者模式不需要,例如观察者的列表可以直接由被观察者维护。不过两者即使被混用,互相替代,通常不影响表达。
  • MQ,中间件级别的消息队列(e.g. ActiveMQ,RabbitMQ),可以认为是发布订阅模式的一个具体体现。事件驱动->发布订阅->MQ,从抽象到具体。
  • java和spring中都拥有Event的抽象,分别代表了语言级别和三方框架级别对事件的支持。
  • EventSourcing这个概念就要关联到领域驱动设计,DDD对事件驱动也是非常地青睐,领域对象的状态完全是由事件驱动来控制,由其衍生出了CQRS架构,具体实现框架有AxonFramework。
  • Nginx可以作为高性能的应用服务器(e.g. openResty),以及Nodejs事件驱动的特性,这些也是都是事件驱动的体现。

本文涵盖的内容主要是前面4点。

Spring对Event的支持

Spring的文档对Event的支持翻译之后描述如下:

ApplicationContext通过ApplicationEvent类和ApplicationListener接口进行事件处理。 如果将实现ApplicationListener接口的bean注入到上下文中,则每次使用ApplicationContext发布ApplicationEvent时,都会通知该bean。 本质上,这是标准的观察者设计模式。

而在spring4.2之后,提供了注解式的支持,我们可以使用任意的java对象配合注解达到同样的效果,首先来看看不适用注解如何在Spring中使用事件驱动机制。

定义业务需求:用户注册后,系统需要给用户发送邮件告知用户注册成功,需要给用户初始化积分;隐含的设计需求,用户注册后,后续需求可能会添加其他操作,如再发送一条短信等等,希望程序具有扩展性,以及符合开闭原则。

如果不使用事件驱动,代码可能会像这样子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class UserService {

   

    @Autowired

    EmailService emailService;

    @Autowired

    ScoreService scoreService;

    @Autowired

    OtherService otherService;

    public void register(String name) {

        System.out.println("用户:" + name + " 已注册!");

        emailService.sendEmail(name);

        scoreService.initScore(name);

        otherService.execute(name);

    }

   

}

要说有什么毛病,其实也不算有,因为可能大多数人在开发中都会这么写,喜欢写同步代码。但这么写,实际上并不是特别的符合隐含的设计需求,假设增加更多的注册项service,我们需要修改register的方法,并且让UserService注入对应的Service。而实际上,register并不关心这些“额外”的操作,如何将这些多余的代码抽取出去呢?便可以使用Spring提供的Event机制。

定义用户注册事件

1

2

3

4

5

public class UserRegisterEvent extends ApplicationEvent{

    public UserRegisterEvent(String name) { //name即source

        super(name);

    }

}

ApplicationEvent是由Spring提供的所有Event类的基类,为了简单起见,注册事件只传递了name(可以复杂的对象,但注意要了解清楚序列化机制)。

定义用户注册服务(事件发布者)

1

2

3

4

5

6

7

8

9

10

11

12

@Service // <1>

public class UserService implements ApplicationEventPublisherAware { // <2>

    public void register(String name) {

        System.out.println("用户:" + name + " 已注册!");

        applicationEventPublisher.publishEvent(new UserRegisterEvent(name));// <3>

    }

    private ApplicationEventPublisher applicationEventPublisher; // <2>

    @Override

    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { // <2>

        this.applicationEventPublisher = applicationEventPublisher;

    }

}

<1> 服务必须交给Spring容器托管
<2> ApplicationEventPublisherAware是由Spring提供的用于为Service注入ApplicationEventPublisher事件发布器的接口,使用这个接口,我们自己的Service就拥有了发布事件的能力。
<3> 用户注册后,不再是显示调用其他的业务Service,而是发布一个用户注册事件。

定义邮件服务,积分服务,其他服务(事件订阅者)

1

2

3

4

5

6

7

@Service // <1>

public class EmailService implements ApplicationListener<UserRegisterEvent> { // <2>

    @Override

    public void onApplicationEvent(UserRegisterEvent userRegisterEvent) {

        System.out.println("邮件服务接到通知,给 " + userRegisterEvent.getSource() + " 发送邮件...");// <3>

    }

}

<1> 事件订阅者的服务同样需要托管于Spring容器
<2> ApplicationListener<E extends ApplicationEvent>接口是由Spring提供的事件订阅者必须实现的接口,我们一般把该Service关心的事件类型作为泛型传入。
<3> 处理事件,通过event.getSource()即可拿到事件的具体内容,在本例中便是用户的姓名。
其他两个Service,也同样编写,实际的业务操作仅仅是打印一句内容即可,篇幅限制,这里省略。

编写启动类

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@SpringBootApplication

@RestController

public class EventDemoApp {

    public static void main(String[] args) {

        SpringApplication.run(EventDemoApp.class, args);

    }

    @Autowired

    UserService userService;

    @RequestMapping("/register")

    public String register(){

        userService.register("kirito");

        return "success";

    }

}

当我们调用userService.register(“kirito”);方法时,控制台打印信息如下:

他们的顺序是无序的,如果需要控制顺序,需要重写order接口,这点不做介绍。其次,我们完成了用户注册和其他服务的解耦,这也是事件驱动的最大特性之一,如果需要在用户注册时完成其他操作,只需要再添加相应的事件订阅者即可。

Spring 对Event的注解支持

上述的几个接口已经非常清爽了,如果习惯使用注解,Spring也提供了,不再需要显示实现

注解式的事件发布者

1

2

3

4

5

6

7

8

9

@Service

public class UserService {

    public void register(String name) {

        System.out.println("用户:" + name + " 已注册!");

        applicationEventPublisher.publishEvent(new UserRegisterEvent(name));

    }

    @Autowired

    private ApplicationEventPublisher applicationEventPublisher;

}

Spring4.2之后,ApplicationEventPublisher自动被注入到容器中,采用Autowired即可获取。

注解式的事件订阅者

1

2

3

4

5

6

7

@Service

public class EmailService {

    @EventListener

    public void listenUserRegisterEvent(UserRegisterEvent userRegisterEvent) {

        System.out.println("邮件服务接到通知,给 " + userRegisterEvent.getSource() + " 发送邮件...");

    }

}

@EventListener注解完成了ApplicationListener<E extends ApplicationEvent>接口的使命。

更多的特性可以参考SpringFramework的文档。

Spring中事件的应用

在以往阅读Spring源码的经验中,接触了不少使用事件的地方,大概列了以下几个,加深以下印象:

  • Spring Security中使用AuthenticationEventPublisher处理用户认证成功,认证失败的消息处理。

1

2

3

4

5

public interface AuthenticationEventPublisher {

   void publishAuthenticationSuccess(Authentication authentication);

   void publishAuthenticationFailure(AuthenticationException exception,

         Authentication authentication);

}

  • Hibernate中持久化对象属性的修改是如何被框架得知的?正是采用了一系列持久化相关的事件,如DefaultSaveEventListener,DefaultUpdateEventListener,事件非常多,有兴趣可以去org.hibernate.event包下查看。
  • Spring Cloud Zuul中刷新路由信息使用到的ZuulRefreshListener

1

2

3

4

5

6

7

8

9

10

11

12

private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> {

       ...

        public void onApplicationEvent(ApplicationEvent event) {

            if(!(event instanceof ContextRefreshedEvent) && !(event instanceof RefreshScopeRefreshedEvent) && !(event instanceof RoutesRefreshedEvent)) {

                if(event instanceof HeartbeatEvent && this.heartbeatMonitor.update(((HeartbeatEvent)event).getValue())) {

                    this.zuulHandlerMapping.setDirty(true);

                }

            } else {

                this.zuulHandlerMapping.setDirty(true);

            }

        }

    }

  • Spring容器生命周期相关的一些默认Event

1

ContextRefreshedEvent,ContextStartedEvent,ContextStoppedEvent,ContextClosedEvent,RequestHandledEvent

。。。其实吧,非常多。。。

总结

本文暂时只介绍了Spring中的一些简单的事件驱动机制,相信如果之后再看到Event,Publisher,EventListener一类的单词后缀时,也能立刻和事件机制联系上了。再阅读Spring源码时,如果发现出现了某个Event,但由于不是同步调用,所以很容易被忽视,我一般习惯下意识的去寻找有没有提供默认的Listener,这样不至于漏掉一些“隐藏”的特性。下一篇文章打算聊一聊分布式场景下,事件驱动使用的注意点。

公众号刚刚创立,如果觉得文章不错,希望能分享到您的朋友圈,如果对文章有什么想法和建议,可以与我沟通。

 

===========================================================================================================================================================================================================================================================================

领域事件与事件溯源

  事件代表过去发生的事件,事件既是技术架构概念,也是业务概念。以事件为驱动的编程模型称为事件驱动架构EDA。

  一个事件代表某个已经发生的事情,在计算机系统中,事件是由一个对象表达,其包含有关事件的数据,比如发生的时间,地点等等。这个事件对象可以存在在一个消息或数据库记录或其他组件的形式中,这样一个对象称为"一个事件"。事件本身是不可变的值对象。  

  事件在技术架构上应用能提供无堵塞的高并发性能,如Nginx和Node.js,而Vert.x. 比 Node.js快好几倍?其他还有Event Stream Processing如Esper等,结合DDD实现的CQRS等。

  事件概念业务系统中应用,诞生领域事件和EventSourcing等DDD实现方式:通过引入事件,类似服务概念一样,跨越业务和技术鸿沟,同时又能表达面向函数编程思维。在业务上将事件和领域驱动设计DDD结合在一起,可以形成统一语言DSL,事件是触发状态变化的根源。

  领域事件是领域中发生的事件。如CustomerRelocated, CargoShipped, or InventoryLossageRecorded. 领域事件将领域模型的改变显式化,突出暴露出来。如下图:

领域事件

 

事务日志

  几乎所有数据库都支持高可用性集群,大多数数据库对系统一致性模型提供一个易于理解的方式,保证强一致性模型的安全方式是维持数据库事务操作的有序日志,理论上理由非常简单,一个事务日志是一系列数据更新操作的动作有序记录集合,当其他节点从主节点获得这个事务日志时,能够按照这种有序动作集合重新播放这些操作,从而更新自己所在节点的数据库状态,当这个事务日志完成后,次节点的状态最终会和主节点状态一致,

  这种事务日志非常类似于财务中记账模型,或者类似银行储蓄卡打印出来的流水账,哪天存入一笔钞票(更新操作),哪天又提取了一笔钞票(更新操作),最后当前余额是多少(代表数据库当前状态)。

  

Event Sourcing

  Event sourcing事件溯源是借鉴数据库事务日志的一种数据持久方式,在事务日志中记录导致状态变化的一系列领域事件。通过持久化记录改变状态的事件,通过重新播放获得状态改变的历史。 事件回放可以返回系统到任何状态。

  在ES中,事务单元变得更细粒度,使用一系列有序的事件来代表存储在数据库中的领域模型状态,一旦一个事件被加入事件日志,它就不能被移走或重新排序,事件被认为是不可变的,事件序列只能被追加方式存储。

 

  由于事件流本身具有逻辑上严格次序性,因此使用统一的事件流(事务日志)能够很自然实现事务机制,无需额外ACID机制或2PC之类同步强硬方式。

 

函数式编程

  在微服务等无状态应用架构中,我们不是需要状态时就发出命令从数据库中查询获得,这样,可变的状态会遍布整个应用代码中,带来很多副作用,而我们将这些状态操作统一为事件流声明式订阅,订阅了某个事件流,通过重播事件流中各个事件一直到最新最后的事件,也就获得了最终的状态。函数式编程Stream风格为这种播放提供了方便,具体Reactive框架有RxJS、React.js、RxJava、Reactor等等。

Spring Cloud Stream实战

  这种实现其实已经在Reactive前端中有着同样实现思路,见:为什么要使用GraphQL和Falcor?,应用程序(微服务)将可变的状态被限定在一个单个的序列化对象中,从而整个应用就变成了无态,可变状态不会扩散到整个应用代码的各个本地变量中。

 

事件建模

  我们甚至可以使用领域事件直接对业务需求进行事件建模,通过事件功能的发现挖掘需求中深层次的概念。动态流的事件模型加上结合DDD的聚合实体 状态 和上下文场景Bounded context,我们实际上统一了需求分析和软件设计两个阶段的语言,使用这套统一语言分析需求以后,能够直接落地为代码,如下图是总结了在Jdon多位牛人的思想后的Jdon分析法:

event

  该图表达了用户操作者和被操作者事物之间的本质关系,以用户和购物车为案例,从购物车这个事物角度看:领域聚合实体表达的是购物车这个事物的分析设计方法,以一种静态结构性来表达事物;从用户购买者这个角度看:用户将选购的商品放入购物车,删除购物车已有商品,这些都是用户的操作行为,每一个操作行为相当于发出一个个命令command,在一定场景中转化为事件,事件会改变购物车状态,这是一种以动态行为(面向函数)来表达与人有关的需求。

  事件建模原理,找出意图 执行和结果范式:

  对应意图 执行和结果范式的事件风暴图:

区块链

 

   通过引入事件概念,可以实现多线程并发到分布式去中心化计算的平滑过渡,区块链本质上就是一种事件链。 通常区块链是指一种分布式账簿,账本其实是记账明细,记录着每笔进出明细,这些发生的每笔明细其实代表系统发生的事件,因此,记账明细其实就是事件日志,区块链其实就是一种分布式事件链,只不过另外增加了一层安全层。

   从CRUD编程切换到事件溯源和区块链编程

 

工作流

   事件是BPMN流程建模元素,表示在流程过程中“发生”的事情,事件会影响流程的走向,事件主要分开始事件、中间事件和结束事件,所谓中间事件就是位于开始和结束之间的事件类型。什么是BPMN事件?

   DDD、领域事件和BPMN流程和Saga可以有机结合在一起:

Jdon框架正在逐步实现上述思想,期待你共同参与。

相关文章

建模风暴(使用领域事件作为用户故事的建模案例)

事件风暴将掀起一场新革命

事件模型-下一个前沿

什么是事件驱动架构EDA?

BPMN开始结束事件的最佳实践

Jdon分析法

领域模型的行为设计

日志是每个软件工程师关心的统一数据抽象

通过实体快照实现事件建模

为什么使用Event Sourcing?

面向事件数据库Event Oriented Databases: 一种新的持久范式

Apache Kafka简单介绍

依赖注入与事件编程

事件驱动编程

Go Reactive宣言

Go 1.5的并发特性与案例(事件与转账)

使用Apache Samza对数据库进行彻底的"调教"

Lagom是一个集成ES/CQRS的Reactive微服务框架

比特币区块链是一种分布式的事件流日志

微服务的最终一致性与事件流

使用Spring Cloud和Reactor在微服务中实现EventSourcing

Twitter的分布式日志DistributedLog

如何理解Stream processing, Event sourcing, Reactive, CEP?

业务流程的新实现:微服务和事件编排

通过事件风暴和DDD建立微服务时优先考虑事件

事件风暴将掀起一场新革命

两个领域事件驱动的开源项目介绍

如何设计实现真正的响应式微服务系统?

事件是一等公民

比特币区块链是一种分布式的事件流日志

从微服务到工作流:Jet订单系统演变过程分享

 

相关话题

#领域事件 #工作流专题 #Saga流程管理器 #Event Sourcing

#Reactive专题 #CQRS专题 #Actor模型 #异步编程

#EDA专题 #无服务器架构 #分布式事务 #业务与系统分析

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值