Atlas源码剖析(十一)

读写分离和负载均衡

在分析完语句后,做好分表的准备工作之后,Atlas会调用check_flags函数检查传过来的语句是否为GET_LOCKAUTOCOMMITSQL_CALC_FOUND_ROWS语句;

 

注解:

aGET_LOCK

get_lock(str,timeout)

设法使用字符串str给定的名字得到一个锁,超时为timeout秒。若成功得到锁,则返回1,若操作超时则返回0(例如,由于另一个客户端已提前封锁了这个名字),若发生错误则返回null (诸如缺乏记忆或线程mysqladmin kill被断开)。假如你有一个用get_lock()得到的锁,当你执行release_lock()或你的连接断开(正常或非正常)时,这个锁就会解除。

这个函数可用于执行应用程序锁或模拟记录锁定。名称被锁定在服务器范围内。假如一个名字已经被一个客户端封锁,get_lock()会封锁来自另一个客户端申请封锁同一个名字的任何请求。这使对一个封锁名达成协议的客户端使用这个名字合作执行建议锁。然而要知道它也允许不在一组合作客户端中的一个客户端封锁名字,不论是服役的还是非故意的,这样阻止任何合作中的客户端封锁这个名字。一个减少这种情况发生的办法就是使用数据库特定的或应用程序特定的封锁名。例如,使用db_name.strapp_name.str形式的封锁名。

 

bAUTOCOMMIT

set autocommit=0,指事务非自动提交,也就是不自动提交,自此句执行以后,每个SQL语句或者语句块所在的事务都需要显示"commit"才能提交事务。mysql如果开了set autocommit=0,那么所有的语句一定是在一个事务里,在这种情况下,如果使用连接池,并且在查询之前没有rollback或者set autocommit=1则会出现不致错误。因为根据mysql的默认事务级别,一致性读,这时将取不到这个事务被开户前的数据。另外set autocommit = 0,会自动提交前一个事务,因此正确的做法是rollback, set autocommit =0,完成之后再set autocommit = 1;

 

cSQL_CALC_FOUND_ROWS:

在很多分页的程序中都这样写:

SELECT COUNT(*) from `table` WHERE ......; 查出符合条件的记录总数

SELECT * FROM `table` WHERE ...... limit M,N; 查询当页要显示的数据

这样的语句可以改成:

SELECT SQL_CALC_FOUND_ROWS * FROM `table` WHERE ...... limit M, N;

SELECT FOUND_ROWS();

这样只要执行一次较耗时的复杂查询可以同时得到与不带limit同样的记录条数

第二个SELECT返回一个数字,指示了在没有LIMIT子句的情况下,第一个SELECT返回了多少行(若上述的SELECT语句不包括SQL_CALC_FOUND_ROWS选项,则使用LIMIT 和不使用时,FOUND_ROWS() 可能会返回不同的结果)。

 

如果调用了get_lock函数,在选择后台时,会选择可写后台(master),并且在调用release_lock之前,会一直保持这个后台连接,不将其放回连接池;

如果使用了set autocommit=0,在选择后台时,会选择可写后台(master),并且在调用set autocomit = 1之前,会一直保持这个后台连接,不将其放回连接池;

如果使用了sql_calc_found_rows,则说明下一条语句将会是select found_rows(),因此会保持后台连接到下一条语句执行后;

void check_flags(GPtrArray* tokens, network_mysqld_con* con) {
  con->is_in_select_calc_found_rows = FALSE;

  sql_token** ts = (sql_token**)(tokens->pdata);
  guint len = tokens->len;

  if (len > 2) {
    if (ts[1]->token_id == TK_SQL_SELECT && 
     strcasecmp(ts[2]->text->str, "GET_LOCK") == 0) {
            gchar* key = ts[4]->text->str;
            if (!g_hash_table_lookup(con->locks, key)) 
        g_hash_table_add(con->locks, g_strdup(key));
        }

        if (len > 4) {  //SET AUTOCOMMIT = {0 | 1}
            if (ts[1]->token_id == TK_SQL_SET && ts[3]->token_id == TK_EQ) {
                if (strcasecmp(ts[2]->text->str, "AUTOCOMMIT") == 0) { 
                    char* str = ts[4]->text->str;
                    if (strcmp(str, "0") == 0) con->is_not_autocommit = TRUE;
                    else if (strcmp(str, "1") == 0) con->is_not_autocommit = FALSE;
                }
            }
        }
    }

    guint i;
    for (i = 1; i < len; ++i) {
        sql_token* token = ts[i];
        if (ts[i]->token_id == TK_SQL_SQL_CALC_FOUND_ROWS) {
            con->is_in_select_calc_found_rows = TRUE;
            break;
        }
    }
}


当还未选择后台连接时,这时会根据分析结果来进行后台选择,也即读写分离;

如果连接没有处在事务当中,没有设置autocommit等于0,也没有调用get_lock,这时如果语句类型是COM_QUERY,则根据分析语句的类型来进行读写分离,如果是COM_INIT_DBCOM_SET_OPTION,则从只读后台中选;其他的情况则从可写后台中选一个。

if (con->server == NULL) {
    int backend_ndx = -1;

  if (!con->is_in_transaction && !con->is_not_autocommit &&
        g_hash_table_size(con->locks) == 0) {
        if (type == COM_QUERY) {
            backend_ndx = rw_split(tokens, con);
            //g_mutex_lock(&mutex);
            send_sock = network_connection_pool_lua_swap(con, backend_ndx);
            //g_mutex_unlock(&mutex);
        } else if (type == COM_INIT_DB || type == COM_SET_OPTION) {
            backend_ndx = wrr_ro(con);
            //g_mutex_lock(&mutex);
            send_sock = network_connection_pool_lua_swap(con, backend_ndx);
            //g_mutex_unlock(&mutex);
        }
    }

    if (send_sock == NULL) {
        backend_ndx = idle_rw(con);
        //g_mutex_lock(&mutex);
        send_sock = network_connection_pool_lua_swap(con, backend_ndx);
        //g_mutex_unlock(&mutex);
    }    
    con->server = send_sock;
}    


进行读写分离的函数rw_split首先检查tokens的个数是否小于2(只有一个的时候是begin,说明是事务)或是否调用了get_lock,如果是,则直接使用可写后台进行处理;

接着清理注释,如果第一个注释的内容是MASTER/*MASTER*/),则直接使用可写后台;

在清理完后注释后,再分析语句的类型,如果是selectsetexplainshowdesc语句,则使用只读后台,否则使用可写后台;

int rw_split(GPtrArray* tokens, network_mysqld_con* con) {
    if (tokens->len < 2 || g_hash_table_size(con->locks) > 0) return idle_rw(con);

    sql_token* first_token = tokens->pdata[1];
    sql_token_id token_id = first_token->token_id;

    if (token_id == TK_COMMENT) {
        if (strcasecmp(first_token->text->str, "MASTER") == 0) { 
            return idle_rw(con);
        } else {
            guint i = 1; 
            while (token_id == TK_COMMENT && ++i < tokens->len) {
                first_token = tokens->pdata[i];
                token_id = first_token->token_id;
            }
        }
    }

  if (token_id == TK_SQL_SELECT || token_id == TK_SQL_SET ||
      token_id == TK_SQL_EXPLAIN || token_id == TK_SQL_SHOW ||
      token_id == TK_SQL_DESC) {
        return wrr_ro(con);
    } else {
        return idle_rw(con);
    }
}


Atlas在挑选只读后台时,进行了一定的负载均衡,在对只读后台进行配置时,可以设置每一台机器的权重,权重越大的被选中的机率越大。但是跟后台配置的顺序有关,代码如下:

int wrr_ro(network_mysqld_con *con) {
    guint i;

    network_backends_t* backends = con->srv->priv->backends;
    g_wrr_poll* rwsplit = backends->global_wrr;
    guint ndx_num = network_backends_count(backends);

    // set max weight if no init
    if (rwsplit->max_weight == 0) {
        for(i = 0; i < ndx_num; ++i) {
            network_backend_t* backend = network_backends_get(backends, i);
            if (backend == NULL) continue;
            if (rwsplit->max_weight < backend->weight) {
                rwsplit->max_weight = backend->weight;
                rwsplit->cur_weight = backend->weight;
            }
        }
    }

    guint max_weight = rwsplit->max_weight;
    guint cur_weight = rwsplit->cur_weight;
    guint next_ndx   = rwsplit->next_ndx;

    // get backend index by slave wrr
    gint ndx = -1;
    for(i = 0; i < ndx_num; ++i) {
        network_backend_t* backend = network_backends_get(backends, next_ndx);
        if (backend == NULL) goto next;

        network_connection_pool* pool = chassis_event_thread_pool(backend);
        if (pool == NULL) goto next;

        if (backend->type == BACKEND_TYPE_RO &&
          backend->weight >= cur_weight && 
          backend->state == BACKEND_STATE_UP) ndx = next_ndx;

    next:
            if (next_ndx == ndx_num - 1) {
                --cur_weight;
                next_ndx = 0;

                if (cur_weight == 0) cur_weight = max_weight;
            } else {
                ++next_ndx;
            }

            if (ndx != -1) break;
    }

    rwsplit->cur_weight = cur_weight;
    rwsplit->next_ndx = next_ndx;
    return ndx;
}


挑选可写后台的函数idle_rw,从第1个后台开始遍历,取对应线程的pool,并检查后台的类型和状态,得到第一个满足条件的可写后台;

int idle_rw(network_mysqld_con* con) {
    int ret = -1;
    guint i;

    network_backends_t* backends = con->srv->priv->backends;

    guint count = network_backends_count(backends);
    for (i = 0; i < count; ++i) {
        network_backend_t* backend = network_backends_get(backends, i);
        if (backend == NULL) continue;

        network_connection_pool* pool = chassis_event_thread_pool(backend);
        if (pool == NULL) continue;

        if (backend->type == BACKEND_TYPE_RW && 
          backend->state == BACKEND_STATE_UP) {
            ret = i; 
            break;
        }    
    }    

    return ret; 
}



你可能感兴趣的:(Atlas)