首页 > 技术文章 > 记录下这周的mysql调优工作

lsm19870508 2016-04-15 15:35 原文

    这周一至周四基本都在做mysql的测试和调优工作,包括erlang端对mysql的写入测试,到今天为止暂且告一段落,下周先做下其他的开发。
    测试环境
    使用的测试环境是aliyun的杭州节点,
    CPU:8核
    内存:8GB
    带宽:5MB
    数据盘:100GB

    Erlang版本:OTP18
    mysql版本:mysql 5.7.11

    测试工具:sysbench,mysqlslap
    Sysbench部分测试:
    prepare的语句:

    ./sysbench --test=/root/sysbench/tests/db/oltp.lua --oltp_tables_count=10 --oltp-table-size=800000 --db-driver=mysql --mysql-socket=/data/mysql/mysql.sock --mysql-user=自己的用户名  --mysql-password='自己的密码' --mysql-db=test_qps prepare

    run的语句:
    ./sysbench --test=/root/sysbench/tests/db/oltp.lua --oltp_tables_count=10 --oltp-table-size=800000 --oltp-read-only=off --max-requests=0 --num-threads=128 --oltp-dist-type=uniform --max-time=600 --report-interval=10 --db-driver=mysql --mysql-socket=/data/mysql/mysql.sock --mysql-user=自己的用户名  --mysql-password='自己的密码' --mysql-db=test_qps run > test_result.log

    
  

  mysqlslap的简单测试
    测试表的sql:

CREATE TABLE `account` (
  `id` int(11) NOT NULL default '0',

  `name` varchar(250) default NULL,

  `password` varchar(250) default NULL,

  `last_login_time` int(11) NOT NULL default '0',

  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

总感觉我不太会用mysqlslap,这样测试的结果有不少问题。还是倾向于用sysbench进行测试。
 Erlang写的测试代码,用于测试emysql效率:

%%%-------------------------------------------------------------------

%%% @author Administrator

%%% @copyright (C) 2016, <COMPANY>

%%% @doc

%%%

%%% @end

%%% Created : 14. 四月 2016 16:28

%%%-------------------------------------------------------------------

-module(multi_thread_test).

-author("Administrator").


%% API

-export([start/2,run/3,run_sql/2,recv/1]).



-record(state,{running=0,start_time,total_count_all,sql_count_each_process}).

%% 启动ProcessCount个进程,每个进程执行SqlCountEachProcess次sql 操作

start(TotalCount,SqlCountEachProcess)->

  emysql:execute(default,<<"delete from account">>),

  CurrentTime=time_utility:longunixtime(),

  spawn_link(?MODULE,run,[TotalCount,SqlCountEachProcess,#state{start_time=CurrentTime,

    total_count_all=TotalCount,

    sql_count_each_process=SqlCountEachProcess}]).


recv(#state{running=0,start_time=StartTime, total_count_all=ProcessCount, sql_count_each_process=SqlCountEachProcess})->

  CurrentTime = time_utility:longunixtime(),

  Usedtime = CurrentTime-StartTime,

  io:format("process_count:~p sql count each process:~p used time:~p~n",[ProcessCount,SqlCountEachProcess,Usedtime]);

recv(#state{running=Running}=State)->

  receive

    done->

      recv(State#state{running=Running-1})

  end.


run(0,_SqlCountEachProcess,#state{}=State)->

  recv(State);

run(TotalCount,SqlCountEachProcess,#state{running=Running}=State) when (TotalCount>0) ->

  Parent =self(),

  spawn(fun()-> run_sql(SqlCountEachProcess,Parent)end),

  run(TotalCount-SqlCountEachProcess,SqlCountEachProcess,State#state{running=Running+1}).


run_sql(0,Parent)->

  Parent!done;

run_sql(SqlCountEachProcess,Parent) ->

  L = lists:seq(1,SqlCountEachProcess),

  [test2() || _<-L],

  run_sql(SqlCountEachProcess-SqlCountEachProcess ,Parent).


test_prepare()->

  Rand = util:rand(1,10000),

  emysql:execute(default,account_replace,[Rand]).


test1()->

  Rand = util:rand(1,10000),

  Sql = io_lib:format(<<"REPLACE INTO account(id) values (~p)">>,[Rand]).


test2()->

  emysql:execute(default,<<"REPLACE INTO account(id) values (floor(RAND()*10000));">>).

运行结果:

是mysqlslap效率的二分之一。

本周测试用的部分代码:

game_db_writer.erl
这个文件实现的是一个队列性质的mysql写入器,做的操作是redis队列中取需要写入的sql,然后一条条的写入mysql

 

%%%-------------------------------------------------------------------
%%% @author 李世铭
%%% @copyright (C) April 1st,2016, <COMPANY>
%%% @doc
%%% 负责redis->mysql同步的写线程
%%% @end
%%% Created : 01. 四月 2016 15:02
%%%-------------------------------------------------------------------
-module(game_db_writer).
-author("Administrator").

-behaviour(gen_fsm).
-include("db_config.hrl").
-include("error_log.hrl").
-include("config_keys.hrl").

%% API
-export([start_link/0]).

-export([write_sql/0]).

%% gen_fsm callbacks
-export([init/1,
  writing/2,
  writing/3,
  handle_event/3,
  handle_sync_event/4,
  handle_info/3,
  terminate/3,
  code_change/4]).

-define(SERVER, ?MODULE).
-define(MAX_PACKET,4096).%%mysql5.6默认允许的最大的包上限
-define(TIMEOUT_SPAN, 1000).%%休眠间隔
-define(ZERO_SPAN,0).%%立即执行

-record(state, {try_times=0}).%%重试次数

%%%===================================================================
%%% API
%%%===================================================================
%%写一条sql语句
write_sql()->
  StartTime = time_utility:longunixtime(),
  io:format("Start Writing Time is ~p!~n",[StartTime]),
  gen_fsm:send_event(?MODULE,{write_a_sql}).

%%--------------------------------------------------------------------
%% @doc
%% Creates a gen_fsm process which calls Module:init/1 to
%% initialize. To ensure a synchronized start-up procedure, this
%% function does not return until Module:init/1 has returned.
%%
%% @end
%%--------------------------------------------------------------------
-spec(start_link() -> {ok, pid()} | ignore | {error, Reason :: term()}).
start_link() ->
  gen_fsm:start_link({local, ?SERVER}, ?MODULE, [], []).

%%%===================================================================
%%% gen_fsm callbacks
%%%===================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Whenever a gen_fsm is started using gen_fsm:start/[3,4] or
%% gen_fsm:start_link/[3,4], this function is called by the new
%% process to initialize.
%%
%% @end
%%--------------------------------------------------------------------
-spec(init(Args :: term()) ->
  {ok, StateName :: atom(), StateData :: #state{}} |
  {ok, StateName :: atom(), StateData :: #state{}, timeout() | hibernate} |
  {stop, Reason :: term()} | ignore).
init([]) ->
  io:format("db_writer is ready!~n"),
  {ok, writing, #state{},?ZERO_SPAN}.
  %%{ok,writing,#state{}}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% There should be one instance of this function for each possible
%% state name. Whenever a gen_fsm receives an event sent using
%% gen_fsm:send_event/2, the instance of this function with the same
%% name as the current state name StateName is called to handle
%% the event. It is also called if a timeout occurs.
%%
%% @end
%%--------------------------------------------------------------------
-spec(writing(Event :: term(), State :: #state{}) ->
  {next_state, NextStateName :: atom(), NextState :: #state{}} |
  {next_state, NextStateName :: atom(), NextState :: #state{},
    timeout() | hibernate} |
  {stop, Reason :: term(), NewState :: #state{}}).
writing(timeout,State)->
  do_write(State);
writing(_Event, State) ->
  do_write(State).

%%--------------------------------------------------------------------
%% @private
%% @doc
%% There should be one instance of this function for each possible
%% state name. Whenever a gen_fsm receives an event sent using
%% gen_fsm:sync_send_event/[2,3], the instance of this function with
%% the same name as the current state name StateName is called to
%% handle the event.
%%
%% @end
%%--------------------------------------------------------------------
-spec(writing(Event :: term(), From :: {pid(), term()},
    State :: #state{}) ->
  {next_state, NextStateName :: atom(), NextState :: #state{}} |
  {next_state, NextStateName :: atom(), NextState :: #state{},
    timeout() | hibernate} |
  {reply, Reply, NextStateName :: atom(), NextState :: #state{}} |
  {reply, Reply, NextStateName :: atom(), NextState :: #state{},
    timeout() | hibernate} |
  {stop, Reason :: normal | term(), NewState :: #state{}} |
  {stop, Reason :: normal | term(), Reply :: term(),
    NewState :: #state{}}).
writing(_Event, _From, State) ->
  Reply = ok,
  {reply, Reply, writing, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Whenever a gen_fsm receives an event sent using
%% gen_fsm:send_all_state_event/2, this function is called to handle
%% the event.
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_event(Event :: term(), StateName :: atom(),
    StateData :: #state{}) ->
  {next_state, NextStateName :: atom(), NewStateData :: #state{}} |
  {next_state, NextStateName :: atom(), NewStateData :: #state{},
    timeout() | hibernate} |
  {stop, Reason :: term(), NewStateData :: #state{}}).
handle_event(_Event, StateName, State) ->
  {next_state, StateName, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Whenever a gen_fsm receives an event sent using
%% gen_fsm:sync_send_all_state_event/[2,3], this function is called
%% to handle the event.
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_sync_event(Event :: term(), From :: {pid(), Tag :: term()},
    StateName :: atom(), StateData :: term()) ->
  {reply, Reply :: term(), NextStateName :: atom(), NewStateData :: term()} |
  {reply, Reply :: term(), NextStateName :: atom(), NewStateData :: term(),
    timeout() | hibernate} |
  {next_state, NextStateName :: atom(), NewStateData :: term()} |
  {next_state, NextStateName :: atom(), NewStateData :: term(),
    timeout() | hibernate} |
  {stop, Reason :: term(), Reply :: term(), NewStateData :: term()} |
  {stop, Reason :: term(), NewStateData :: term()}).
handle_sync_event(_Event, _From, StateName, State) ->
  Reply = ok,
  {reply, Reply, StateName, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_fsm when it receives any
%% message other than a synchronous or asynchronous event
%% (or a system message).
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_info(Info :: term(), StateName :: atom(),
    StateData :: term()) ->
  {next_state, NextStateName :: atom(), NewStateData :: term()} |
  {next_state, NextStateName :: atom(), NewStateData :: term(),
    timeout() | hibernate} |
  {stop, Reason :: normal | term(), NewStateData :: term()}).
handle_info(_Info, StateName, State) ->
  {next_state, StateName, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_fsm when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_fsm terminates with
%% Reason. The return value is ignored.
%%
%% @end
%%--------------------------------------------------------------------
-spec(terminate(Reason :: normal | shutdown | {shutdown, term()}
| term(), StateName :: atom(), StateData :: term()) -> term()).
terminate(_Reason, _StateName, _State) ->
  ok.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @end
%%--------------------------------------------------------------------
-spec(code_change(OldVsn :: term() | {down, term()}, StateName :: atom(),
    StateData :: #state{}, Extra :: term()) ->
  {ok, NextStateName :: atom(), NewStateData :: #state{}}).
code_change(_OldVsn, StateName, State, _Extra) ->
  {ok, StateName, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================
%%进行实际的写操作
do_write(State)->
  case State#state.try_times>0 of
    true->
      %%说明上次的消息未写入成功,从中转区取消息
      Result = redis:get(?CURR_WRITING_MSG),
      case Result of
        {ok,SzMsg} ->
          Msg = db_utility:unpack_data(SzMsg);
        _->
          {ok,Msg} = game_db_queue:dequeue(?MYSQL_WRITE_LIST),
          ?LOG_ERROR("REDIS SYSTEM ERROR!!!Cannot load Msg from game_frame:mysql_writing_msg")
      end;
    _->
      {ok,Msg} = game_db_queue:dequeue(?MYSQL_WRITE_LIST)
  end,
  case Msg of
    %%队列已空
    undefined->
      CurrTime = time_utility:longunixtime(),
      io:format("end writing test time is:~w~n",[{CurrTime}]),
      {next_state,writing,#state{},?TIMEOUT_SPAN};
    _->
      do_write(Msg,State)
  end.

do_write(Msg,State)->
  %%先将取出来的消息存入中转区
  redis:set(?CURR_WRITING_MSG,db_utility:pack_data(Msg)),
  IsPrepare = Msg#db_queue_msg.prepare,
  case IsPrepare of
    true->
      %%如果预编译过
      SqlId = Msg#db_queue_msg.prepare_atom,
      SqlArgs = Msg#db_queue_msg.prepare_param,
      PoolId = Msg#db_queue_msg.poolid,
      Result = mysql:run_prepare(PoolId,SqlId,SqlArgs);
    _->
      %%如果没有
      PoolId = Msg#db_queue_msg.poolid,
      Sql = Msg#db_queue_msg.sql,
      Result = mysql:execute(PoolId,Sql)
  end,
  case Result of
    {ok,_}->
      %%写入成功后标记数据过期时间
      Redis_expir_time = game_config:lookup_keys([?CF_DB_QUEUE, <<"redis_expir_time">>]),
      redis:expire(Msg#db_queue_msg.redis_key, integer_to_list(util:floor(3600 * Redis_expir_time))),
      %%然后中转区标记为<<"successful">>,表示写成功
      redis:set(?CURR_WRITING_MSG,<<"successful">>),
      {next_state,writing,#state{},?ZERO_SPAN};
    _->
      RetryTimes = State#state.try_times,
      case RetryTimes>=?MAX_MYSQL_RETRY_TIME of
        true->
          %% 如果写代码次数超过上限
          %% 单独写一个log,方便查找log
          ?LOG_ERROR("Max MySQL retry times reached, Msg is: ~p",
            [[Msg]]),
          {next_state,writing,#state{},?ZERO_SPAN};
        _->
          {next_state,writing,#state{try_times=RetryTimes + 1},?ZERO_SPAN}
      end
  end.

 

game_db_writer2:
这个文件是上面那个文件的升级版,区别是一次性取一定数量(宏定义现在是100)的sql语句,自行进行sql拼接一次性写入,写入失败的语句会重新拼接进行写入,效率比上面那种方法能提高不少,但是如果多个mysql节点的话处理起来会比较麻烦

%%%-------------------------------------------------------------------
%%% @author 李世铭
%%% @copyright (C) April 1st,2016, <COMPANY>
%%% @doc
%%% 负责redis->mysql同步的写线程
%%% @end
%%% Created : 01. 四月 2016 15:02
%%%-------------------------------------------------------------------
-module(game_db_writer2).
-author("Administrator").

-behaviour(gen_fsm).
-include("db_config.hrl").
-include("error_log.hrl").
-include("config_keys.hrl").

%% API
-export([start_link/0]).

-export([write_sql/0]).

%% gen_fsm callbacks
-export([init/1,
  writing/2,
  writing/3,
  handle_event/3,
  handle_sync_event/4,
  handle_info/3,
  terminate/3,
  code_change/4]).

-define(SERVER, ?MODULE).
-define(MAX_PACKET,4096).%%mysql5.6默认允许的最大的包上限
-define(TIMEOUT_SPAN, 1000).%%休眠间隔
-define(ZERO_SPAN,0).%%立即执行

-record(state, {try_times=0}).%%重试次数

%%%===================================================================
%%% API
%%%===================================================================
%%写一条sql语句
write_sql()->
  StartTime = time_utility:longunixtime(),
  io:format("Start Writing Time is ~p!~n",[StartTime]),
  gen_fsm:send_event(?MODULE,{write_a_sql}).

%%--------------------------------------------------------------------
%% @doc
%% Creates a gen_fsm process which calls Module:init/1 to
%% initialize. To ensure a synchronized start-up procedure, this
%% function does not return until Module:init/1 has returned.
%%
%% @end
%%--------------------------------------------------------------------
-spec(start_link() -> {ok, pid()} | ignore | {error, Reason :: term()}).
start_link() ->
  gen_fsm:start_link({local, ?SERVER}, ?MODULE, [], []).

%%%===================================================================
%%% gen_fsm callbacks
%%%===================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Whenever a gen_fsm is started using gen_fsm:start/[3,4] or
%% gen_fsm:start_link/[3,4], this function is called by the new
%% process to initialize.
%%
%% @end
%%--------------------------------------------------------------------
-spec(init(Args :: term()) ->
  {ok, StateName :: atom(), StateData :: #state{}} |
  {ok, StateName :: atom(), StateData :: #state{}, timeout() | hibernate} |
  {stop, Reason :: term()} | ignore).
init([]) ->
  io:format("db_writer is ready!~n"),
  {ok, writing, #state{},?ZERO_SPAN}.
  %%{ok,writing,#state{}}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% There should be one instance of this function for each possible
%% state name. Whenever a gen_fsm receives an event sent using
%% gen_fsm:send_event/2, the instance of this function with the same
%% name as the current state name StateName is called to handle
%% the event. It is also called if a timeout occurs.
%%
%% @end
%%--------------------------------------------------------------------
-spec(writing(Event :: term(), State :: #state{}) ->
  {next_state, NextStateName :: atom(), NextState :: #state{}} |
  {next_state, NextStateName :: atom(), NextState :: #state{},
    timeout() | hibernate} |
  {stop, Reason :: term(), NewState :: #state{}}).
writing(timeout,State)->
  do_write(State);
writing(_Event, State) ->
  do_write(State).

%%--------------------------------------------------------------------
%% @private
%% @doc
%% There should be one instance of this function for each possible
%% state name. Whenever a gen_fsm receives an event sent using
%% gen_fsm:sync_send_event/[2,3], the instance of this function with
%% the same name as the current state name StateName is called to
%% handle the event.
%%
%% @end
%%--------------------------------------------------------------------
-spec(writing(Event :: term(), From :: {pid(), term()},
    State :: #state{}) ->
  {next_state, NextStateName :: atom(), NextState :: #state{}} |
  {next_state, NextStateName :: atom(), NextState :: #state{},
    timeout() | hibernate} |
  {reply, Reply, NextStateName :: atom(), NextState :: #state{}} |
  {reply, Reply, NextStateName :: atom(), NextState :: #state{},
    timeout() | hibernate} |
  {stop, Reason :: normal | term(), NewState :: #state{}} |
  {stop, Reason :: normal | term(), Reply :: term(),
    NewState :: #state{}}).
writing(_Event, _From, State) ->
  Reply = ok,
  {reply, Reply, writing, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Whenever a gen_fsm receives an event sent using
%% gen_fsm:send_all_state_event/2, this function is called to handle
%% the event.
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_event(Event :: term(), StateName :: atom(),
    StateData :: #state{}) ->
  {next_state, NextStateName :: atom(), NewStateData :: #state{}} |
  {next_state, NextStateName :: atom(), NewStateData :: #state{},
    timeout() | hibernate} |
  {stop, Reason :: term(), NewStateData :: #state{}}).
handle_event(_Event, StateName, State) ->
  {next_state, StateName, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Whenever a gen_fsm receives an event sent using
%% gen_fsm:sync_send_all_state_event/[2,3], this function is called
%% to handle the event.
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_sync_event(Event :: term(), From :: {pid(), Tag :: term()},
    StateName :: atom(), StateData :: term()) ->
  {reply, Reply :: term(), NextStateName :: atom(), NewStateData :: term()} |
  {reply, Reply :: term(), NextStateName :: atom(), NewStateData :: term(),
    timeout() | hibernate} |
  {next_state, NextStateName :: atom(), NewStateData :: term()} |
  {next_state, NextStateName :: atom(), NewStateData :: term(),
    timeout() | hibernate} |
  {stop, Reason :: term(), Reply :: term(), NewStateData :: term()} |
  {stop, Reason :: term(), NewStateData :: term()}).
handle_sync_event(_Event, _From, StateName, State) ->
  Reply = ok,
  {reply, Reply, StateName, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_fsm when it receives any
%% message other than a synchronous or asynchronous event
%% (or a system message).
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_info(Info :: term(), StateName :: atom(),
    StateData :: term()) ->
  {next_state, NextStateName :: atom(), NewStateData :: term()} |
  {next_state, NextStateName :: atom(), NewStateData :: term(),
    timeout() | hibernate} |
  {stop, Reason :: normal | term(), NewStateData :: term()}).
handle_info(_Info, StateName, State) ->
  {next_state, StateName, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_fsm when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_fsm terminates with
%% Reason. The return value is ignored.
%%
%% @end
%%--------------------------------------------------------------------
-spec(terminate(Reason :: normal | shutdown | {shutdown, term()}
| term(), StateName :: atom(), StateData :: term()) -> term()).
terminate(_Reason, _StateName, _State) ->
  ok.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @end
%%--------------------------------------------------------------------
-spec(code_change(OldVsn :: term() | {down, term()}, StateName :: atom(),
    StateData :: #state{}, Extra :: term()) ->
  {ok, NextStateName :: atom(), NewStateData :: #state{}}).
code_change(_OldVsn, StateName, State, _Extra) ->
  {ok, StateName, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================
%%进行实际的写操作
do_write(State)->
  case State#state.try_times>0 of
    true->
      %%说明上次的消息未写入成功,从中转区取消息
      Result = redis:get(?CURR_WRITING_MSG_MULT),
      case Result of
        {ok,SzMsg} ->
          MsgList = db_utility:unpack_data(SzMsg);
        _->
          {ok,MsgList} = game_db_queue:dequeue(?MYSQL_MULTI_WRITE_NUM,?MYSQL_WRITE_LIST_MULT),
          ?LOG_ERROR("REDIS SYSTEM ERROR!!!Cannot load Msg from game_frame:mysql_writing_msg")
      end;
    _->
      {ok,MsgList} = game_db_queue:dequeue(?MYSQL_MULTI_WRITE_NUM,?MYSQL_WRITE_LIST_MULT)
  end,
  case MsgList of
    %%队列已空
    []->
      CurrTime = time_utility:longunixtime(),
      io:format("end writing test time is:~w~n",[{CurrTime}]),
      {next_state,writing,#state{},?TIMEOUT_SPAN};
    _->
      do_write(MsgList,State)
  end.

do_write(MsgList,State)->
  %%先将取出来的消息存入中转区
  redis:set(?CURR_WRITING_MSG_MULT,db_utility:pack_data(MsgList)),
  F = fun(X,{_,FinalSql})->
        Sql = X#db_queue_msg.sql,
        PoolId = X#db_queue_msg.poolid,
        %%之所以逆序是因为取出来的时候逆序存放
        {PoolId,<<Sql/binary,";",FinalSql/binary>>}
      end,
  {PoolId,Sql} = lists:foldr(F,{default,<<>>},MsgList),
  Result = emysql:execute(PoolId,Sql),
  Result1 = lists:zip(MsgList,Result),
  F1 = fun({Msg,ETM}, Res) ->
    case ETM of
      {ok_packet,_,_,NID,_,_,_}->
        Res;
      {result_packet,_,_,RS,_} ->
        Res;
      {error_packet,_,_,_,DB_ERROR_MSG} ->
        Res ++ [Msg]
    end
  end,
  if
    is_list(Result)->
      LeftMsgList = lists:foldl(F1, [], Result1);
  true->
      LeftMsgList = lists:foldl(F1, [], [Result1])
  end,
  case LeftMsgList of
    []->
      %%写入成功后标记数据过期时间
      Redis_expir_time = game_config:lookup_keys([?CF_DB_QUEUE, <<"redis_expir_time">>]),
      [redis:expire(Msg#db_queue_msg.redis_key, integer_to_list(util:floor(3600 * Redis_expir_time))) || Msg<-MsgList],
      %%然后中转区标记为<<"successful">>,表示写成功
      redis:set(?CURR_WRITING_MSG_MULT,<<"successful">>),
      {next_state,writing,#state{},?ZERO_SPAN};
    _->
      RetryTimes = State#state.try_times,
      case RetryTimes>=?MAX_MYSQL_RETRY_TIME of
        true->
          %% 如果写代码次数超过上限
          %% 单独写一个log,方便查找log
          ?LOG_ERROR("Max MySQL retry times reached, Msg is: ~p",
            [[MsgList]]),
          {next_state,writing,#state{},?ZERO_SPAN};
        _->
          redis:set(?CURR_WRITING_MSG_MULT,LeftMsgList),
          {next_state,writing,#state{try_times=RetryTimes + 1},?ZERO_SPAN}
      end
  end.

game_db_writer3:
这个放弃了状态机的方式,之前我对前两种的效率很不满意,怀疑是gen_fsm内部的超时机制消耗了大量的事情,才写了这个非otp的写入,但经测试发现,otp内部消耗的时间基本可以忽略不计。

%%%-------------------------------------------------------------------
%%% @author Administrator
%%% @copyright (C) 2016, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 14. 四月 2016 11:13
%%%-------------------------------------------------------------------
-module(game_db_writer3).
-author("Administrator").
-include("db_config.hrl").
-include("error_log.hrl").
-include("config_keys.hrl").

%% API
-export([write_sql/0]).

write_sql()->
  StartTime = time_utility:longunixtime(),
  io:format("Start Writing Time is ~p!~n",[StartTime]),
  do_write(0),
  ok.

%%进行实际的写操作
do_write(TryTimes)->
  case TryTimes>0 of
    true->
      %%说明上次的消息未写入成功,从中转区取消息
      Result = redis:get(?CURR_WRITING_MSG),
      case Result of
        {ok,SzMsg} ->
          Msg = db_utility:unpack_data(SzMsg);
        _->
          {ok,Msg} = game_db_queue:dequeue(?MYSQL_WRITE_LIST),
          ?LOG_ERROR("REDIS SYSTEM ERROR!!!Cannot load Msg from game_frame:mysql_writing_msg")
      end;
    _->
      {ok,Msg} = game_db_queue:dequeue(?MYSQL_WRITE_LIST)
  end,
  case Msg of
    %%队列已空
    undefined->
      CurrTime = time_utility:longunixtime(),
      io:format("end writing test time is:~w~n",[{CurrTime}]),
      timer:sleep(1000);
      %%do_write(0);
    _->
      do_write(Msg,TryTimes)
  end.

do_write(Msg,TryTimes)->
  %%先将取出来的消息存入中转区
  redis:set(?CURR_WRITING_MSG,db_utility:pack_data(Msg)),
  IsPrepare = Msg#db_queue_msg.prepare,
  case IsPrepare of
    true->
      %%如果预编译过
      SqlId = Msg#db_queue_msg.prepare_atom,
      SqlArgs = Msg#db_queue_msg.prepare_param,
      PoolId = Msg#db_queue_msg.poolid,
      Result = mysql:run_prepare(PoolId,SqlId,SqlArgs);
    _->
      %%如果没有
      PoolId = Msg#db_queue_msg.poolid,
      Sql = Msg#db_queue_msg.sql,
      Result = mysql:execute(PoolId,Sql)
  end,
  case Result of
    {ok,_}->
      %%写入成功后标记数据过期时间
      Redis_expir_time = game_config:lookup_keys([?CF_DB_QUEUE, <<"redis_expir_time">>]),
      redis:expire(Msg#db_queue_msg.redis_key, integer_to_list(util:floor(3600 * Redis_expir_time))),
      %%然后中转区标记为<<"successful">>,表示写成功
      redis:set(?CURR_WRITING_MSG,<<"successful">>),
      do_write(0);
    _->
      case TryTimes>=?MAX_MYSQL_RETRY_TIME of
        true->
          %% 如果写代码次数超过上限
          %% 单独写一个log,方便查找log
          ?LOG_ERROR("Max MySQL retry times reached, Msg is: ~p",
            [[Msg]]),
          do_write(0);
        _->
          do_write(Msg,TryTimes+1)
      end
  end.

db_test.erl
这个主要是配合进行压测的erl,包括简单的单线程sql效率测试

%%%-------------------------------------------------------------------
%%% @author 李世铭
%%% @copyright (C) 2016, <COMPANY>
%%% @doc
%%% 测试数据库各种写方法的效率
%%% @end
%%% Created : 05. 四月 2016 16:48
%%%-------------------------------------------------------------------
-module(db_test).
-author("Administrator").
-include("db_config.hrl").

%% API
-export([test_db_multi_write/1,test_prepare_write/1,test_directly_write/1]).
-export([test_directly_select/1,test_prepare_select/1]).
-export([test_db_write/1,test_db_write_single/1]).
-export([test_eprof_start/0,test_eprof_end/0]).

test_db_multi_write(N)->
  CurrTime = time_utility:longunixtime(),

  L = lists:seq(1,N),
  F = fun(X,Res)->
        Rand = util:rand(1,1000000),
        Sql = mysql:make_insert_sql(account,["id"],[Rand]),
        SzSql = list_to_binary(Sql),
        case Res==<<"">> of
          true->
            SzSql;
          _->
            <<Res/binary,";",SzSql/binary>>
        end
      end,
  FinalSql = lists:foldl(F,<<>>,L),
  Result  = emysql:execute(default,FinalSql),
  EndTime = time_utility:longunixtime(),
  io:format("Cost time is:~w~n",[{EndTime - CurrTime}]).

test_prepare_write(N)->
  CurrTime = time_utility:longunixtime(),
  L = lists:seq(1,N),
  F = fun(X)->
    Rand = util:rand(1,10000),
    emysql:execute(default,account_replace,[Rand])
      end,
  [F(X) || X<-L],
  EndTime = time_utility:longunixtime(),
  io:format("Cost time is:~w~n",[{EndTime - CurrTime}]).

test_directly_write(N)->
  CurrTime = time_utility:longunixtime(),
  L = lists:seq(1,N),
  F = fun(X)->
    Rand = util:rand(1,10000),
    Sql = mysql:make_replace_sql(account,["id"],[Rand]),
    emysql:execute(default,Sql)
  end,
  [F(X) || X<-L],
  EndTime = time_utility:longunixtime(),
  io:format("Cost time is:~w~n",[{EndTime - CurrTime}]).

test_directly_select(N)->
  CurrTime = time_utility:longunixtime(),
  L = lists:seq(1,N),
  F = fun(X)->
    Sql = io_lib:format(<<"select * from account where id=~p">>,[X]),
    emysql:execute(default,Sql)
    end,
  [F(X) || X<-L],
  EndTime = time_utility:longunixtime(),
  io:format("Cost time is:~w~n",[{EndTime - CurrTime}]).

test_prepare_select(N)->
  emysql:prepare(account_select,"select * from account where id=?"),
  CurrTime = time_utility:longunixtime(),
  L = lists:seq(1,N),
  F = fun(X)->
    emysql:execute(default,account_select,[X])
      end,
  [F(X) || X<-L],
  EndTime = time_utility:longunixtime(),
  io:format("Cost time is:~w~n",[{EndTime - CurrTime}]).

test_db_write()->
  Rand = util:rand(1,100000),
  Sql = mysql:make_replace_sql(account,["id"],[Rand]),
  SzSql = conversion_utility:to_binary(Sql),
  State = #db_queue_msg{redis_key = <<"TEST_HINCR">>,sql = SzSql},
  %%State = #db_queue_msg{redis_key = <<"TEST_HINCR">>,prepare = true,prepare_atom = account_replace,prepare_param = [Rand]},
  game_db_queue:enqueue(State,?MYSQL_WRITE_LIST_MULT).

test_db_write(N)->
  test_eprof_start(),
  CurrTime = time_utility:longunixtime(),
  io:format("enqueue start time is:~w~n",[{CurrTime}]),
  L = lists:seq(1,N),
  [test_db_write() || X<-L],
  CurrTime1 = time_utility:longunixtime(),
  io:format("enqueue end time is:~w~n",[{CurrTime1}]),
  test_eprof_end(),
  ok.

test_db_write_single()->
  Rand = util:rand(1,100000),
  Sql = mysql:make_replace_sql(account,["id"],[Rand]),
  SzSql = conversion_utility:to_binary(Sql),
  State = #db_queue_msg{redis_key = <<"TEST_HINCR">>,sql = SzSql},
  %%State = #db_queue_msg{redis_key = <<"TEST_HINCR">>,prepare = true,prepare_atom = account_replace,prepare_param = [Rand]},
  game_db_queue:enqueue(State,?MYSQL_WRITE_LIST).

test_db_write_single(N)->
  test_eprof_start(),
  CurrTime = time_utility:longunixtime(),
  io:format("enqueue start time is:~w~n",[{CurrTime}]),
  L = lists:seq(1,N),
  [test_db_write_single() || X<-L],
  CurrTime1 = time_utility:longunixtime(),
  io:format("enqueue end time is:~w~n",[{CurrTime1}]),
  test_eprof_end(),
  ok.

test_eprof_start()->
  eprof:start(),
  eprof:start_profiling([self()]).

test_eprof_end()->
  eprof:stop_profiling(),
  eprof:log(test_match),
  eprof:analyze(),
  eprof:stop().

个人感觉:
1.队列方式进行写入的时候无法充分的利用cpu,因为队列必然是单线程,800%CPU只能使用50%的时候mysql的效率一定不高。
2.批量写入并不见得一定好用,会带来很多其它的问题,如果不是特殊需求,没必要非得批量写入。
3.尽量购买rdb而不是自己搭建mysql服务器,20倍的iops差距真的很坑。 

 最后附上我自己测试用的my.cnf

# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/5.6/en/server-configuration-defaults.html
# *** DO NOT EDIT THIS FILE. It's a template which will be copied to the
# *** default location during install, and will be replaced if you
# *** upgrade to a newer version of MySQL.
[mysqld]

# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.

# Remove leading # to turn on a very important data integrity option: logging
# changes to the binary log between backups.

# These are commonly set, remove the # and set as required.
basedir = /usr
datadir = /data/mysql
socket = /data/mysql/mysql.sock
pid-file = /data/mysql/mysql_pidfile.pid
log-error = /data/mysql/mysql_errorlog.err
# port = .....
server_id = 1
# socket = .....

# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M 

log_bin = /data/binlog/mysql_binlog
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES 

max_connections=4000
key_buffer_size=200M
low_priority_updates=1
table_open_cache = 8000
back_log=1500
query_cache_type=0
query_cache_limit = 1M
query_cache_size=256M
table_open_cache_instances=16

# files
innodb_file_per_table = ON
innodb_log_file_size=1024M
innodb_log_files_in_group = 3
innodb_open_files=4000

# buffers
innodb_buffer_pool_size=4096M
innodb_buffer_pool_instances=32
innodb_log_buffer_size=64M
join_buffer_size=32K
sort_buffer_size=32K

# innodb
innodb_checksums=0
innodb_doublewrite=0
innodb_support_xa=0
innodb_thread_concurrency=0
innodb_flush_log_at_trx_commit=2
innodb_max_dirty_pages_pct=50
innodb_use_native_aio=1
innodb_stats_persistent = 1

# perf special
innodb_adaptive_flushing = 1
innodb_flush_neighbors = 0
innodb_read_io_threads = 4
innodb_write_io_threads = 4
innodb_io_capacity = 4000
innodb_purge_threads=1
innodb_adaptive_hash_index=0

# monitoring
innodb_monitor_enable = '%'
performance_schema=OFF

 

推荐阅读