用过MYSQL数据库的人都知道,MYSQL的一般普通机器上用Insert语句操作,插入的速度2k条左右,如果随着数据的膨胀或者表数量的增多,速度会急剧下降。公司有个老项目,是记录仪器产生的数据,该仪器每秒产生一条数据,由于是很多台仪器,所以数据量很大,通过Insert语句根本达不到要求。
现在公司准备改进这个项目,计划在一台普通的pc机做为服务器的话(普通的机器就是主流配置一般机器:主频3G的CPU,4G的内存等等,差不多这样子),要求每秒要记录2000台设备产生的数据,就是每秒插入速度要达到2000+,每天要记录1亿七千多万条数据。我刚接手这个项目,感觉老板疯了,就是把MYSQL数据库累翻了也不可能完成的任务。试想,如此大的数据量,必定要对数据进行拆分,必须动态建立表,随着表数量的增多,数据库的膨胀,速度肯定变慢,更变态的是,这个仪器产生的数据中,每一条还要存储一个512字节二进制数据,算了一下,每天的数据大小是120G多,加上索引乱七八糟的150G都不止,并且更更变态是,要求这台服务器还要可以运行其他软件,当时一听心里骂老板真抠门,多弄几台服务器能死啊。对于一台普通的机器,对于一个小型数据库,如果能有此能力,真是不敢想象啊。
老板就是老板,执意为之。做为喽啰只有领命了。开始建议老板做集群,读写分离,老板一听要多买机器,增加成本,马上否定了,说多一台机器,要多加2000台仪器,nnd,那加服务器有什么用!没办法就慢慢摸索。
闲言少叙,在我的实践过程中,实验了10几种方法,历时半年有余,最终成功了,故分享给大家。以下数据都是在普通pc机上测试所得。
首先说明一下这个仪器。这是个做网络信号测试的仪器,每台仪器都会有一个IGM号和一个Port号,这两个号加起来就是该仪器的唯一标识。
由于数据量很大,需要拆分数据。通过无数次实验可得,MYSQL数据库每张表的记录达到千万级别的时候,速度会降低,数据库中的表的数量达到500张以上时速度也会下降,当达到几万张表的时候,速度相当于蜗牛,每秒仅可以Insert100条数据左右。这样粗略算了一下,只能根据时间纵向拆分,每小时一张表,这样每张表就只有720万条数据,2个月的表也不到1500张表,何况普通机器根本记录不了2个月的数据,最多一个月就了不得了,一个月的数据可是好几个T啊。
数据拆分方案定下来后,就是找插入数据的方案了,普通Insert肯定是不行了。LOAD DATA INFILE的速度是insert的数倍,所以采用LOAD做为方案是可行的。关于LOAD DATA INFILE的用法手册里说的很清除,不多说了。LOAD速度虽然快,但是也有问题,每条数据中的这512个字节的二进制数据不知道是什么东西,也许是图片,也许是个txt文件,怎么样保证把这个二进制数据写到文件中,再LOAD到数据库中是正确的呢。直接把这个二进制数据写下来肯定是错误的。我在实验中存了一张照片,再用
SELECT * INTO OUTFILE 'data.txt' FIELDS TERMINATED BY ',' FROM table;
查看编码,一头雾水,不知所以,没办法只有啃源码了,还好MYSQL数据库是开源的能下到代码。通过编译调试,终于找到了SELECT * INTO OUTFILE的编码方式,如下:
#region ReadOnly Field private readonly byte ESCAPE_CHAR = 0x5C; //'\' private readonly byte NULL_CHAR = 0x4E; //'N' private readonly byte FIELD_SEP_CHAR = 0x22; //ENCLOSED BY '"' private readonly byte FIELD_TERM_CHAR = 0x2C; //FIELDS TERMINATED BY ',' private readonly byte LINE_SEP_CHAR1 = 0x0D; //LINES TERMINATED BY '\r' private readonly byte LINE_SEP_CHAR2 = 0x0A; //LINES TERMINATED BY '\n' private readonly byte ZERO_CHAR = 0x30; //'0' private readonly string LOAD_DATA_DIRECTORY = "LoadData"; private readonly string FILE_NAME_JOINT_STRING = "___"; #endregion private bool NEED_ESCAPING(byte x) { return (x == ESCAPE_CHAR || x == FIELD_SEP_CHAR || x == LINE_SEP_CHAR1 || x == 0); }
private void RecodeData(byte[] data)
{
if (data != null)
{
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;
this._buffer_cursor++;
for (int i = 0; i < data.Length; i++)
{
byte x = data[i];
byte y = (i + 1) < data.Length ? data[i + 1] : data[i];
if (NEED_ESCAPING(x))
{
this._bufferPool[this._buffer_cursor] = ESCAPE_CHAR;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = (x > 0 ? x : ZERO_CHAR);
this._buffer_cursor++;
}
else
{
this._bufferPool[this._buffer_cursor] = x;//x > 127 ? unchecked((byte)((int)x - 256)) :
this._buffer_cursor++;
}
}
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;
this._buffer_cursor++;
}
else
{
this._bufferPool[this._buffer_cursor] = ESCAPE_CHAR;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = NULL_CHAR;
this._buffer_cursor++;
}
}
this._bufferPool只是个容器,这里是个很大的byte[]数组,this._buffer_cursor是个表示位置的数字,以上代码就是源码中重新对二进制数据重新编码存储到本地的方式。因为我在调试源代码的时候,固定了数据的格式,即每项数据以双引号(")包括,以逗号分割(,),以换行(\r\n)结束,形式像:"每列值","每列值",..."每列值"\r\n(换行不显示)。知道了如何编码,以下的事情就好办了,直接把数据存到一个大容器里,然后设定一个最大阀值,每当到达阀值时就Load,代码如下:
private void GetByteArrayFromIngressMonitorResultHeaderWithData(IngressMonitorResultHeader rlt, byte[] data)
{
int minuteSecond = rlt.TestTime.Minute * 60 + rlt.TestTime.Second;
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;
this._buffer_cursor++;
byte[] arrayTemp = System.Text.Encoding.Default.GetBytes(minuteSecond.ToString());
AppendByteArray(arrayTemp);
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_TERM_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
arrayTemp = System.Text.Encoding.Default.GetBytes(rlt.PortStatus.ToString());
AppendByteArray(arrayTemp);
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_TERM_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
arrayTemp = System.Text.Encoding.Default.GetBytes(rlt.StartFreq.ToString());
AppendByteArray(arrayTemp);
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_TERM_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
arrayTemp = System.Text.Encoding.Default.GetBytes(rlt.StopFreq.ToString());
AppendByteArray(arrayTemp);
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_TERM_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
arrayTemp = System.Text.Encoding.Default.GetBytes(rlt.DwellTime.ToString());
AppendByteArray(arrayTemp);
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_TERM_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
arrayTemp = System.Text.Encoding.Default.GetBytes(rlt.RBW.ToString());
AppendByteArray(arrayTemp);
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_TERM_CHAR;;
this._buffer_cursor++;
this.RecodeData(data);
this._bufferPool[this._buffer_cursor] = FIELD_TERM_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
arrayTemp = System.Text.Encoding.Default.GetBytes(rlt.SampleIndex.ToString());
AppendByteArray(arrayTemp);
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_TERM_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
arrayTemp = System.Text.Encoding.Default.GetBytes(rlt.IGMUnitNum.ToString());
AppendByteArray(arrayTemp);
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_TERM_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
arrayTemp = System.Text.Encoding.Default.GetBytes(rlt.PortNum.ToString());
AppendByteArray(arrayTemp);
this._bufferPool[this._buffer_cursor] = FIELD_SEP_CHAR;;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = LINE_SEP_CHAR1;
this._buffer_cursor++;
this._bufferPool[this._buffer_cursor] = LINE_SEP_CHAR2;
this._buffer_cursor++;
}
private void WriteDataByteArray(string fileName)
{
FileStream fs = new FileStream(fileName, FileMode.Create);
try
{
fs.Write(this._bufferPool, 0, this._buffer_cursor);
}
catch
{
}
finally
{
fs.Close();
this._buffer_cursor = 0;
}
}
private void LoadTable()
{
while (this._qFiles.Count > 0)
{
string file_table;
lock (this)
{
file_table = this._qFiles.Dequeue();
}
string[] fileName_tbName = file_table.Split(new string[] { FILE_NAME_JOINT_STRING }, StringSplitOptions.None);
string fileName =
this.Load_File_Path.EndsWith("\\") ? (this.Load_File_Path + file_table) : (this.Load_File_Path + Path.DirectorySeparatorChar + file_table);//fileName_tbName[0];
string tbName = fileName_tbName[1];
if (File.Exists(fileName))
{
string fileNameTemp = fileName.Replace(Path.DirectorySeparatorChar.ToString(),
Path.DirectorySeparatorChar.ToString() + Path.DirectorySeparatorChar.ToString());
AGAIN:
string sql = @"LOAD DATA INFILE '" + fileNameTemp + "' INTO TABLE `" + tbName + "` FIELDS TERMINATED BY ',' ENCLOSED BY '\"' LINES TERMINATED BY '\\r\\n';";
try
{
Vetronics.Data.MySqlHelper.ExecuteNonQuery(ref this._connection.Connection, CommandType.Text, sql, null);
}
catch (Exception ex)
{
if (this._connection.Connection.State != ConnectionState.Open)
{
this._connection.ReConnect();
goto AGAIN;
}
}
File.Delete(fileName);
}
}
}
上面第一个方法是把自己定义的类,也就是要存入数据库中的数据转成byte[],第二个方法是到达上限阀值的时候把数组里面的内容存储到本地,这里一定要注意,不可以把内存里的内容重新用utf8之类的格式编码,一定要把内存里的内容原封不动的存到一个文档里,这里可以是记事本之类的记录容器;第三个方法就是Load数据了,上面那个_qFiles是个队列,刚开始打算用多线程,缓存数据与Load互不干扰,把每一次记录到本地的文件名插入队列,这样Load时依次出队Load数据,这样可以大大提高效率,但是由于机器性能问题,最后采用了单线程,保留了这个队列,以后改回来方便。
经测试,多线程的情况下开始可以达到1w条每秒,但是这只是缓存到了本地,至于Load的速度要看你机器的性能了;单线程的情况下,3000+每秒绝对没问题。这样问题解决了,感谢老板的抠门与执着,否则真的不敢想象,小小mysql能干如此大事。
如果你遇到了和我同样的问题,看到了我的这篇文章,如果对你有一些帮助,那么我会感到非常荣幸。也许文章写的比较槽糕,不够明白,如果有什么更好方案,可以提出来大家一起讨论,共同提高。
期待你的参与。