事件、消息、数据驱动编程范式

64 阅读10分钟

事件、消息、数据驱动编程范式-R

事件驱动编程

角色:

  • 事件:相当于一个行为,其中包含与事件源绑定的回调函数,即当该事件源完成后要执行的行为。
  • 事件源:相当于生产者/被观察者,产生事件。
  • 事件处理器:相当于消费者/观察者。
  • 事件循环:负责监听事件源,当事件发生时,触发相应的事件处理器

监控方式:

  1. 使用一个线程轮询该组件

    1. 这种方式容易造成阻塞,CPU资源浪费和响应时间过长。
  2. 使用一个队列来存储发生的事件,有一个消费者不断从中读取事件并调用不同的处理程序,如前端中的onClick()。

    1. 事件(消息)一般各自保存自己的处理函数指针,每个消息都有独立的处理函数。

方式二相当于事件源是生产者,不断生成事件放入消息队列,监听器负责根据事件调用不同的处理程序。

比如书记让秘书过两个小时后通知他去开会,书记是事件源,它产生了一个事件:过两个小时去开会,而秘书则是一个事件队列,他会循环回忆书记的事件,以确保到点能通知书记去完成相应的行为

单线程、多线程、事件驱动编程的对比,每个任务在执行I/O操作时阻塞自身,灰色表示阻塞。

  • 单线程:按顺序执行3个任务,调用I/O阻塞当前线程,造成CPU资源浪费
  • 多线程:每个线程执行一个任务,但在调用I/O时仍造成CPU资源浪费
  • 事件驱动:当遇到代价昂贵的任务时,异步调用该任务,并将当前线程立即返回,继续执行后续的任务。当I/O异步执行完成后,注册一个事件到事件队列,事件循环持续轮训事件队列,检测到该事件则取出,调用其绑定的回调函数执行后续逻辑。

image.png

事件驱动编程步骤:

  1. 应用线程发起​非阻塞 I/O 请求​(如读取文件、发送网络包),同时将 “I/O 完成后的处理逻辑”(回调函数)封装成一个 “事件”,注册到​事件队列
  2. 发起 I/O 的线程不阻塞,立即返回,继续处理其他就绪任务(或被事件循环调度去处理其他事件);
  3. I/O 操作由操作系统内核异步处理(期间 CPU 可用于其他任务,无闲置);
  4. 当 I/O 完成后,操作系统向应用层发送 “I/O 完成通知”,触发对应的 “完成事件” 进入事件队列;
  5. 事件循环(Event Loop) (通常是单线程,如 Node.js 的事件循环)持续轮询事件队列,一旦检测到待处理的 “I/O 完成事件”,就从队列中取出,调用其绑定的回调函数执行后续逻辑;
  6. 回调执行完毕后,事件循环继续轮询下一个事件,循环往复。
实现方式

回调函数: 这是更轻量级的方式,特别是在异步编程中。

Reactive Streams: 如RxJS、Project Reactor,它们提供了更强大的事件流处理能力,背压控制等,其思想根基也来自于观察者模式,但功能远超传统观察者模式。

消息队列/发布-订阅模式: 这是观察者模式的升级版,在解耦方面更彻底。

与观察者模式、发布订阅模式的关系

观察者模式是事件驱动编程的一个实现。编程范式是一个目标,而设计模式则是其中的一种实现方式。

概念角色类比
事件驱动编程架构范式/目标想要建立一个 “应急响应系统” 。核心思想是:有事发生,立刻响应。
观察者模式设计模式/实现手段实现这个系统的一种​具体组织方式:设立一个报警中心(Subject),让各个应急单位(Observers)在中心注册。一旦有警报(事件),中心就通知所有相关单位。
其他模式(如Pub/Sub)另一种实现手段实现该系统的另一种​更解耦的方式:通过一个公共的无线电频道(Message Broker),任何单位可以发布消息,任何单位可以订阅感兴趣的消息。

情况 1:事件驱动可以基于发布订阅模式实现(解耦场景)

情况 2:事件驱动也可以不依赖发布订阅模式(简单场景),使用观察者模式。

消息驱动编程

发布订阅1模式是该编程范式的实现方式之一,与事件驱动编程的区别是,事件源产生事件/消息后,事件/消息由第三方系统放入队列,事件源仅负责消息的生成,而事件处理器责负责从第三方系统中获取消息并处理。

比如作家编写文章之后,由报社来负责收集、发表、销售等,报社会将文章售卖给订阅相关主题的读者。作家通过文章来传达他的想法,而读者通过文章去了解作家要表达的内容。两者通过文章(消息) 完成跨时空的交流。

组件间不直接调用,而是通过发送 / 接收消息协作。

角色:生产者、消费者、消息中间件(比如Kafka、RabbitMQ)。

目标:解耦组件间的通信

数据驱动编程

传统应用: 使用if、for、Switch语句进行硬编码,无法在运行后修改。

数据驱动:由数据驱动行为,数据和行为独立于系统,系统根据不同的数据执行不同的行为。

比如在企业级应用开发中,在数据库中配置菜单和功能,不同的角色显示不同的菜单和功能。

将业务规则和行为控制从硬编码中解放出来,转而由外部数据源(数据库)来驱动程序的运行时的行为和表现。

这种架构使得系统更加灵活、可配置和易于维护,是企业级应用追求的目标之一。

特性传统应用数据驱动编程
控制源代码逻辑外部数据
灵活性编译时/运行时确定运行时可变更
修改方式修改代码重新编译修改数据文件
核心思想"我知道要做什么""根据数据决定做什么"
根据用户鉴权动态生成菜单和功能
工作流程
graph TD
    A[用户登录] --> B[查询用户角色和权限数据]
    B --> C[根据权限数据过滤可用菜单]
    C --> D[动态生成用户界面]
    D --> E[用户交互]
    E --> F[根据权限数据控制功能访问]

数据库表结构

-- 菜单表:定义所有可用菜单项
CREATE TABLE sys_menu (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),        -- 菜单名称
    path VARCHAR(100),       -- 路由路径
    component VARCHAR(100),  -- 前端组件
    parent_id BIGINT,        -- 父级菜单
    sort INT,                -- 排序
    visible BOOLEAN          -- 是否可见
);

-- 角色表:定义角色类型
CREATE TABLE sys_role (
    id BIGINT PRIMARY KEY,
    role_key VARCHAR(50),    -- 角色标识符
    role_name VARCHAR(50)    -- 角色名称
);

-- 角色-菜单关联表:配置数据的核心
CREATE TABLE sys_role_menu (
    role_id BIGINT,
    menu_id BIGINT,
    PRIMARY KEY (role_id, menu_id)
);

-- 用户-角色关联表
CREATE TABLE sys_user_role (
    user_id BIGINT,
    role_id BIGINT
);
业务层接口
// 数据驱动的菜单生成示例
@Service
public class MenuService {
    
    public List<MenuDTO> generateUserMenu(Long userId) {
        // 1. 从数据库查询用户的菜单权限数据
        List<Menu> userMenus = menuMapper.selectMenusByUserId(userId);
        
        // 2. 由数据驱动生成菜单结构
        return buildMenuTree(userMenus); // 菜单结构完全由数据决定
    }
}
动态菜单生成
// 数据驱动的菜单生成示例
@Service
public class MenuService {
    
    public List<MenuDTO> generateUserMenu(Long userId) {
        // 1. 从数据库查询用户的菜单权限数据
        List<Menu> userMenus = menuMapper.selectMenusByUserId(userId);
        
        // 2. 由数据驱动生成菜单结构
        return buildMenuTree(userMenus); // 菜单结构完全由数据决定
    }
}
数据驱动的权限控制
// 基于注解的权限控制 - 由数据库数据驱动
@PreAuthorize("@ss.hasPermission('system:user:list')")
@GetMapping("/user/list")
public Result<List<User>> userList() {
    // 权限判断逻辑由数据库中的权限数据驱动
    List<User> users = userService.listUsers();
    return Result.success(users);
}

// 自定义权限校验服务
@Service("ss")
public class PermissionService {
    
    public boolean hasPermission(String permission) {
        // 从数据库查询当前用户的权限数据
        Set<String> permissions = getCurrentUserPermissions();
        // 由权限数据决定是否允许访问
        return permissions.contains(permission);
    }
}

功能开关的数据驱动

// 数据库中的功能配置表
@Entity
@Table(name = "sys_feature_toggle")
public class FeatureToggle {
    private String featureKey;    // 功能标识
    private boolean enabled;      // 是否启用
    private String allowedRoles;  // 允许的角色
}

// 功能访问控制
@Service
public class FeatureAccessService {
    
    public boolean isFeatureEnabled(String featureKey, User user) {
        // 从数据库读取功能配置数据
        FeatureToggle toggle = featureToggleRepository.findByFeatureKey(featureKey);
        
        if (toggle == null || !toggle.isEnabled()) {
            return false; // 由数据驱动功能开关
        }
        
        // 由数据驱动角色权限
        return user.getRoles().stream()
                .anyMatch(role -> toggle.getAllowedRoles().contains(role.getKey()));
    }
}

消息驱动、事件驱动、发布订阅模式的关联

  • 消息驱动编程 → 必然关联发布订阅模式:消息驱动以 “消息解耦” 为核心,发布订阅模式是实现这一目标的核心机制,二者几乎是 “绑定关系”;
  • 事件驱动编程 → 可选关联发布订阅模式:事件驱动以 “响应事件” 为核心,简单场景用 “回调 / 观察者模式”,复杂解耦场景才用发布订阅模式,二者是 “灵活关联关系”。

观察者模式通过 “接口依赖解耦被观察者和观察者(被观察者只需知道观察者的接口,无需知道具体实现) ;事件驱动编程通过 “事件注册解耦事件源和处理器(事件源无需知道谁会处理事件,只需触发事件即可)。

参考资料

事件驱动编程、消息驱动编程、数据驱动编程_和事件驱动编程对应的-CSDN博客

deepseek:物理层与数据传输层知识解析 - DeepSeek

豆包:Java 可重入锁及相关方法 - 豆包

(25 封私信 / 64 条消息) 数据驱动编程:让你的嵌入式代码更优雅更灵活! - 知乎

事件驱动编程:基础知识与实践1.背景介绍 事件驱动编程(Event-Driven Programming,EDP)是一种 - 掘金

Footnotes

  1. 发布订阅

    是一个软件架构中的一种消息范式,也是经典的设计模式

    发布者不直接与订阅者联系,发布者将要发布的消息交由中心管理订阅者也是根据自己的情况,按需订阅中心中的消息。

    比如作家(生产者) 写好文章交给报社(发布订阅中心) ,由报社负责销售 。你(订阅者) 订阅了某个专题(主题)报纸,你订阅的报纸报社安排人送到你家。

    好处:生产者与订阅者解耦,生产者发送消息后不需要关注消息的状态,仅需要专注于自己的工作。订阅者订阅消息后,只需要等消息到达后进行处理,不需要关注消息如何到达。由发布订阅中心负责消息的存储、转发、传输。

    角色:

    • 发布者:
    • 发布订阅中心:该范式的重点,
    • 订阅者:

    应用:消息队列,jdk9的响应式流

    背压:减缓发布者发布消息的速度