数据库内核月报

数据库内核月报 - 2022 / 10

源码分析 · MySQL Event 源码分析

Author: yifei

背景介绍

mysql5.1版本开始引进Event概念。Event,顾名思义,属于时间触发器,相比于triggers的事件触发器来说,Event更加类似与linux crontab计划任务,Event中可以使用单独的SQL语句或调用使用存储过程,在某一特定的时间点,触发相关的SQL语句或存储过程。

使用Event调用SQL语句时,用户需要具有Event权限和执行该SQL的权限。Event权限的设置保存在mysql.user表和mysql.db表的Event_priv字段中。

Eventprocedure配合使用的时候,用户需要具有create routine权限和调用存储过程执行时的excute权限,存储过程调用具体的SQL语句时,需要用户具有执行该SQL的权限。Event可以通过以下几种方式查看其详细信息:

本文基于官方mysql-8.0.31版本的代码,对Event的实现进行分析。

源码分析

在使用Event的过程中,可以看到Event的主要功能包括:

数据结构

Event的实现架构主要包括Event,Event_schedular,Event_queue,Event_queue_element,Event_db_repository,Event_dd,Event_parse_data等几个类,主要关系如下: UML 当用户输入Event相关语句时,首先会通过Event_parse_data将SQL转换为Event结构,随后在mysql_execute_command中调用Events::create_event创建新的Event事件。Events类是Event功能对外暴露的静态接口类,其中包括一个Event_schedularEvent_queueEvent_queue是包括所有Event事件的队列,Event_schedular是Event线程的后台调度器,负责从Event_queue中拿出对应的Event运行。Event_queue中包括一个由Event_queue_element构成的vector数组,每一个Event_queue_element都是一个Event事件,它继承于Event_basic,包括单个Event事件所需要的所有参数。

Events::create_event的过程中,还会调用Event_db_repository::create_event,把对应的Event事件插入到dd中,用来帮助用户在Information_schema中查看对应的Event。Event_db_repository也是一个接口类,其实际的实现还是在dd_event类中。

执行流程

调度流程

// Event init
|--> Events::init
|    |--> opt_event_scheduler == Events::EVENTS_DISABLED // 检查环境变量
|    |--> check_access  // 检查权限
|    |--> event_queue = new Event_queue()    // 创建新的 queue
|    |--> scheduler = new Event_scheduler(event_queue)   // 创建新的调度器
|    |--> event_queue->init_queue()    // 初始化队列
|    |--> load_events_from_db(thd, event_queue)   // 从存储层读取对应的Event

// 创建新的Events
|--> Events::create_event
|    |--> check_access  // 检查权限
|    |--> Event_db_repository::create_event  // 创建Event的dd信息
|    |--> event_queue->create_event  // 创建Event_queue_element
|    |    |--> new_element->compute_next_execution_time(thd);  // 计算新的event的执行时间
|    |    |--> queue.push(new_element);                        // 把新的event插入到队列中
|    |    |--> mysql_cond_broadcast(&COND_queue_state);  // 通知Event_schedular
|    |--> end

// Events_schedular 后台执行
|--> Events::start
|    |--> scheduler->start
|    |    |--> scheduler->run
|    |    |    |--> queue->recalculate_activation_times(thd)    // 重新计算所有event的执行时间
|    |    |    |--> while (is_running())
|    |    |    |    |--> get_top_for_execution_if_time(thd, &event_name)  // 获得最早执行的event
|    |    |    |    |    |--> while(true)
|    |    |    |    |    |    |--> if (queue.empty())
|    |    |    |    |    |    |    |--> cond_wait(thd);  // 等待新的event事件插入
|    |    |    |    |    |    |--> endif
|    |    |    |    |    |    |--> top = queue.top()
|    |    |    |    |--> res = execute_top  // 执行event
|    |    |    |--> end loop

Events::init Event功能最开始的初始化入口,如果当前系统的环境变量 event_schedularON,就会启动Event_schedular线程,来控制event的执行。此时,这个线程可以通过 show processlist; 命令查看到。

mysql> show processlist;
+----+-----------------+-----------+-------+---------+--------+-----------------------------+------------------+
| Id | User            | Host      | db    | Command | Time   | State                       | Info             |
+----+-----------------+-----------+-------+---------+--------+-----------------------------+------------------+
|  1 | event_scheduler | localhost | NULL  | Daemon  |      3 | Waiting for next activation | NULL             |
+----+-----------------+-----------+-------+---------+--------+-----------------------------+------------------+
1 rows in set (0.00 sec)

event_queue是一个基于std::vector<>的排序队列,mysql在其中实现了event_queue_element的排序算法,是基于时间的,因此从queue上通过get_topevent一定是最新要执行的。如果当前queue是空的,则会等待新event的插入,并唤醒当前的schedular线程。当拿到最新的Event_queue_element之后,会由Event_worker_thread::run 来真正执行当前的event

实际执行

// Event_worker_thread::run
|-->  post_init_event_thread(thd);   // 初始化当前线程的THD
|-->  mysql_thread_set_secondary_engine(false);   // 设置不支持secondary_engine
|-->  thd->mdl_context.acquire_lock  // 获得mdl锁
|-->  Event_db_repository::load_named_event  // 从dd中读取对应的event的详细信息
|-->  job_data.execute
|    |-->  mysql_reset_thd_for_next_command(thd);  // 重新设执行环境
|    |-->  event_sctx.change_security_context  // 修改event线程的权限
|    |-->  construct_sp_sql(thd, &sp_sql)  // 构建存储过程
|    |-->  thd->set_query(sp_sql.c_ptr_safe(), sp_sql.length());  // 设置query
|    |-->  parse_sql(thd, &parser_state, m_creation_ctx)  // sql parse
|    |-->  sphead->execute_procedure(thd, &empty_item_list)  // 执行存储过程
|    |-->  end:
|    |-->  construct_drop_event_sql(thd, &sp_sql, m_schema_name, m_event_name) // 如果event只执行一次,就删除
|    |-->  event_sctx.restore_security_context  // 重新设置权限

Event的真正执行是靠Event_worker_thread::run来实现的,执行过程是通过construct_sp_sql,把用户创建Event时的sql包在一个存储过程中,按照存储过程的方式去执行。这样做的好处是用户可以在event中使用存储过程,方便创建比较复杂的Event。同时,Event执行前会由系统赋予对应的权限,则是因为Event_worker_thread是由event_schedular创建的,没有对应的权限信息。因此mysql的做法时在create event时检查客户是否有对应的权限,在Event_worker_thread真正执行时,已经可以保证当前sql是有权限的了,所以不需要再一次检查。

总结

总的来说,Event的执行流程还是非常清晰的,其流程如下所示:

Event流程图 Event_queue是一个按事件排序的Event队列,当到达Event的执行时间时,Event_schedular会被唤醒,并从从Event_queu中拿出需要执行的Event,创建新的Event_worker_thread来执行对应的Event。当前Event如果是循环执行的话,会再次被插入到Event_queue中,等待下一次调度。

整个实现的过程还包含了很多的细节,本文都没有进行详细的展开,包括dd的写入,主从集群的Event调度等,后续有机会再展开介绍。