我们经常遇到一个情况,就是网络断开或程序Bug导致COMMIT/ROLLBACK语句没有传到数据库,也没有释放线程,但是线上事务锁定等待严重,连接数暴涨,尤其在测试库这种情况很多,线上也偶有发生,于是想为MySQL增加一个杀掉空闲事务的功能。

那么如何实现呢,通过MySQL Server层有很多不确定因素,最保险还是在存储引擎层实现,我们用的几乎都是InnoDB/XtraDB,所以就基于Percona来修改了,Oracle版的MySQL也可以照着修改。

需求:
1. 一个事务启动,如果事务内最后一个语句执行完超过一个时间(innodb_idle_trx_timeout),就应该关闭链接。
2. 如果事务是纯读事务,因为不加锁,所以无害,不需要关闭,保持即可。
虽然这个思路被Percona的指出Alexey Kopytov可能存在“Even though SELECT queries do not place row locks by default (there are exceptions), they can still block undo log records from being purged.”的问题,但是我们确实有场景SELECT是绝对不能kill的,除非之后的INSERT/UPDATE/DELETE发生了,所以我根据我们的业务特点来修改。跟Percona的Yasufumi Kinoshita和Alexey Kopytov提出过纯SELECT事务不应被kill,现在也得到回复他们也同意这么做。


一、在InnoDB层实现

首先想到这个功能肯定是放在InnoDB Master Thread最方便,Master Thread每秒调度一次,可以顺便检查空闲事务,然后关闭,因为在事务中操作trx->mysql_thd并不安全,所以一般来说最好在InnoDB层换成Thread ID操作,并且InnoDB中除了ha_innodb.cc,其他地方不能饮用THD,所以Master Thread中需要的线程数值,都需要在ha_innodb中计算好传递整型或布尔型返回值给master thread调用。

首先,我们要增加一个参数:idle_trx_timeout,它表示事务多久没有下一条语句发生就超时关闭。
在storage/innodb_plugin/srv/srv0srv.c的“/* plugin options */”注释下增加如下代码注册idle_trx_timeout变量。



static MYSQL_SYSVAR_LONG(idle_trx_timeout, srv_idle_trx_timeout,  PLUGIN_VAR_RQCMDARG,  "If zero then this function no effect, if no-zero then wait idle_trx_timeout seconds this transaction will be closed",  "Seconds of Idle-Transaction timeout",  NULL, NULL, 0, 0, LONG_MAX, 0);


代码往下找在innobase_system_variables结构体内加上:



(idle_trx_timeout),


有了这个变量,我们需要在Master Thread(storage/innodb_plugin/srv/srv0srv.c )中执行检测函数查找空闲事务。在loop循环的if (sync_array_print_long_waits(&waiter, &sema)判断后加上这段判断



if (srv_idle_trx_timeout && trx_sys) {
*  trx;
time_t  now;
:
= time(NULL);
(&kernel_mutex);
= UT_LIST_GET_FIRST(trx_sys->mysql_trx_list); # 从当前事务列表里获取第一个事务
while (trx) { # 依次循环每个事务进行检查
if (trx->conc_state ==
&& trx->mysql_thd
&& innobase_thd_is_idle(trx->mysql_thd)) { # 如果事务还活着并且它的状态时空闲的
= innobase_thd_get_start_time(trx->mysql_thd); # 获取线程最后一个语句的开始时间
= innobase_thd_get_thread_id(trx->mysql_thd); #获取线程ID,因为存储引擎内直接操作THD不安全
if (trx->last_stmt_start != start_time) { # 如果事务最后语句起始时间不等于线程最后语句起始时间说明事务是新起的
->idle_start = now; # 更新事务的空闲起始时间
->last_stmt_start = start_time; # 更新事务的最后语句起始时间
} else if (difftime(now, trx->idle_start) # 如果事务不是新起的,已经执行了一部分则判断空闲时间有多长了
> srv_idle_trx_timeout) { # 如果空闲时间超过阈值则杀掉链接
/* kill the session */
(&kernel_mutex);
(thd_id); # 杀链接
goto rescan_idle;
}
}
= UT_LIST_GET_NEXT(mysql_trx_list, trx); # 检查下一个事务
}
(&kernel_mutex);
}


其中trx中的变量是新加的,在storage/innodb_plugin/include/trx0trx.h的trx_truct加上需要的变量:



struct trx_struct{...
time_t      idle_start;
;
 ...
}


这里有几个函数是自定义的:



(const void* thd);
(const void* thd);
(const void* thd);


这些函数在ha_innodb.cc中实现,需要在storage/innodb_plugin/srv/srv0srv.c头文件定义下加上这些函数的引用形势。

然后在storage/innodb_plugin/handler/ha_innodb.cc 中定义这些函数的实现:



extern "C"
 ibool
(
const void* thd)
/*!< in: thread handle (THD*) */
{
return(((const THD*)thd)->command == COM_SLEEP);
}
extern "C"
 ib_int64_t
(
const void* thd)
/*!< in: thread handle (THD*) */
{
return((ib_int64_t)((const THD*)thd)->start_time);
}
extern "C"
 ulong
(
const void* thd)
{
return(thd_get_thread_id((const THD*) thd));
}


还有最重要的thd_kill函数负责杀线程的,在sql/sql_class.cc中,找个地方定义这个函数:



void thd_kill(ulong id){
*tmp;
(pthread_mutex_lock(&LOCK_thread_count));
<THD> it(threads);
while ((tmp=it++))    {
if (tmp->command == COM_DAEMON || tmp->is_have_lock_thd == 0 ) # 如果是DAEMON线程和不含锁的线程就不要kill了
continue;
if (tmp->thread_id == id)
{
(&tmp->LOCK_thd_data);
break;
}
}
(pthread_mutex_unlock(&LOCK_thread_count));
if (tmp)
{
->awake(THD::KILL_CONNECTION);
(&tmp->LOCK_thd_data);
}
}


为了存储引擎能引用到这个函数,我们要把它定义到plugin中:

include/mysql/plugin.h和include/mysql/plugin.h中加上


void thd_kill(unsigned long id);


如何判定线程的is_have_lock_thd值?首先在THD中加上这个变量(sql/sql_class.cc):



class THD :public Statement,           public Open_tables_state{....  uint16    is_have_lock_thd;....}


然后在SQL的必经之路mysql_execute_command拦上一刀,判断是有锁操作发生了还是事务提交或新起事务。



switch (lex->sql_command) {
case SQLCOM_REPLACE:
case SQLCOM_REPLACE_SELECT:
case SQLCOM_UPDATE:
case SQLCOM_UPDATE_MULTI:
case SQLCOM_DELETE:
case SQLCOM_DELETE_MULTI:
case SQLCOM_INSERT:
case SQLCOM_INSERT_SELECT:
->is_have_lock_thd = 1;
break;
case SQLCOM_COMMIT:
case SQLCOM_ROLLBACK:
case SQLCOM_XA_START:
case SQLCOM_XA_END:
case SQLCOM_XA_PREPARE:
case SQLCOM_XA_COMMIT:
case SQLCOM_XA_ROLLBACK:
case SQLCOM_XA_RECOVER:
->is_have_lock_thd = 0;
break;
}


为了尽可能兼容Percona的补丁,能引用的都引用了Percona的操作,有些函数调用是在层次太多看不下去了就简化了。

二、在Server层实现

在MySQL Dev Team一位开发的提示下,这个功能在Server层实现的话,不仅通用性更广,而且更安全。

不得不说active_transaction()是个好函数,可以返回线程是否在一个事务内,就可以在Server端通用的判断事务了。

如何在Server层实现呢,sql/sql_parse.cc的do_command()函数也是个好函数,连接线程会循环调用do_command()来读取并执行命令,在do_command()函数中,会调用my_net_set_read_timeout(net, thd->variables.net_wait_timeout)来设置线程socket连接超时时间,于是在这里可以下手。
主要代码:



830   /*
831     This thread will do a blocking read from the client which
832     will be interrupted when the next command is received from
833     the client, the connection is closed or "net_wait_timeout"
834     number of seconds has passed
835   */
836   /* Add For Kill Idle Transaction By P.Linux */
837   if (thd->active_transaction())
838   {
839     if (thd->variables.trx_idle_timeout > 0)
840     {
841       my_net_set_read_timeout(net, thd->variables.trx_idle_timeout);
842     } else if (thd->variables.trx_readonly_idle_timeout > 0 && thd->is_readonly_trx)
843     {
844       my_net_set_read_timeout(net, thd->variables.trx_readonly_idle_timeout);
845     } else if (thd->variables.trx_changes_idle_timeout > 0 && !thd->is_readonly_trx)
846     {
847       my_net_set_read_timeout(net, thd->variables.trx_changes_idle_timeout);
848     } else {
849       my_net_set_read_timeout(net, thd->variables.net_wait_timeout);
850     }
851   } else {
852     my_net_set_read_timeout(net, thd->variables.net_wait_timeout);
853   }
854   /* End */


大家看明白了吗?其实这是偷梁换柱,本来在这里是要设置wait_timeout的,先判断线程是不是在事务里,就可以转而实现空闲事务的超时。

trx_idle_timeout 控制所有事务的超时,优先级最高
 trx_changes_idle_timeout 控制非只读事务的超时
 trx_readonly_idle_timeout 控制只读事务的超时

效果:



(none) 08:39:49> set autocommit = 0
, 0 rows affected (0.00 sec)
(none) 08:39:56> set trx_idle_timeout = 5;
, 0 rows affected (0.00 sec)
(none) 08:40:17> use
Database
40:19> insert into perf (info ) values('11');
, 1 row affected (0.00 sec)
40:26> select * from
2006 (HY000): MySQL server has gone awayNo connection.
to reconnect...Connection id:    6
database: perf
+----+------+
| id | info |
+----+------+
|  7 | aaaa |
|  9 | aaaa |
| 11 | aaaa |
+----+------+
3 rows in set (0.00 sec)


完整的patch这里下载:

mysql 未提交事务手动提交 mysql事务不提交_mysql 未提交事务手动提交  server_kill_idle_trx.patch



这个功能Percona已经决定接受这个方案,比他们自己写的方案既更简单也更安全,功能一样,呵呵。