最近有个网友在做gps的一个系统。简单的讲就是,系统接收卫星传回的每辆车的数据,并保存这些数据供以后查询。每条数据的包含信息一般由*MID,CMD,F,DATA#构成。'*'为开头(1个字节),'#'为结尾 (1个字节)。下面就是一条示例数据:
*000000,00,00,155705,A,2202.12,11307.001,10,0,211209#
车ID:000000(MID,6个字节)
命令标示:00(CMD,2个字节)
状态标示:00(超速报警,防盗报警。。。)(F,2个字节)
卫星时间(时分秒:155705)15:57:05(6个字节)
数据是否有效(A有效,V无效)(1个字节)
维度:2202.12
经度:11307.001
速度:10
方向:0(0-360)
卫星时间:(日月年211209)09-12-21 (6个字节)
每辆车大约每40秒有一条数据,一天大约有2000条记录。整个系统由多个服务器组成,设计要求是每台服务器大约负担5万辆车半年的数据插入存储和查询。这就是每天有1亿条数据插入,而且还要不经常查询这个系统实际上是网友(qq号是63731???放飞)的一个系统。他目前是用sql server 做数据存储的。采用定时bcp的方式来处理的,在存储上每个数据库服务器只能负担1万到2万左右的车辆,达不到服务器负担5万辆车的设计要求。其实只考虑bcp数据插入,用sql server 是可以承受更多的数据插入的,但是还要保证数据查询不要过慢,所以我的那个网友最好只好每台服务器负责1万辆车的数据了。他目前的系统每分钟只能提供100次的查询。这也是他最后决定每台服务器只负责1万辆车的原因。经过我对这个系统的分析后,我做了一些改动和优化,试每台服务器可以达到5万辆车的数据存储,数据查询上也达到了每分钟1500-2000次)查询左右。这个速度是5万辆车中随机不同的车号的查询,如果是同一车号的不同时间的查询,可以达到每分钟1万次以上。基本上就测试结果来看,在数据插入变为原来的5倍的情况下,查询也提高了10倍以上。下面我开始说说,我是如何处理的,我的方法其实不是一个好的方法,也没有用任何技术,用的写法是任何一个大学二年级的学生都曾经写过的写法(甚至是非计算机系的学生)。我为什么采用这种写法的原因是:我很懒,而且这种写法也刚好可以达到设计要求。
第一步,解析数据
所有数据是通过udp从卫星上收下来的,很多条保存成一个文本文件,然后交给负责数据插入的程序处理。这种写法为什么是这样,还有为什么是变长的文本数据而不是定长的2进制数据,这个只能说是个历史遗留问题。其实在做项目时,经常遇到很多旧的程序,有时要兼容这些是必须的。先分析原来的程序的写法,他原来的写法用的是下面这个函数
function Split(aValue: string; aDelimiter: Char; StringList: TStrings): string;
var
X: Integer;
S: string;
begin
if StringList = nil then
StringList := TStringList.Create;
StringList.Clear;
S := '';
for X := 1 to Length(aValue) do
begin
if aValue[X] <> aDelimiter then
S := S + aValue[X]
else begin
S := S + aDelimiter;
StringList.Add(TrimLeft(S));
S := '';
end;
end;
Result := S;//未处理的数据
end;
其实这个函数比较中规中矩(后来和网友聊,据说这个也是我随手写给他的,不过我忘记了)。好了我们开始改这里吧。我有个不好的习惯就是不喜欢用string,尤其不喜欢字符串相加。上边的那个函数好像也是我写的,估计那是急着去洗手间的时候写的。下面就是新的代码了。调用的代码为ReadReciveData(ms_text.Memory, ms_text.Position, ms_dat, ReciveData);其中 ms_text和 ms_dat是TMemoryStream对象。
TGetReciveDataProc = procedure(const obj: Pointer; const ReciveData: TReciveData) of object;
function ReadReciveData(buf: pchar; len: integer; obj: Pointer; Proc: TGetReciveDataProc): integer;
var
i, begin_i, ReciveData_bufflen: integer;
p, begin_p: PChar;
ReciveData: TReciveData;
begin
{ 解析文本 速度还可以 2009-12-23
begin_Char: char = '*';
end_Char: char = '#';
}
Result := 0;
begin_p := nil;
p := buf;
for i := 1 to len do
begin
if p^ = begin_Char then
begin
begin_p := p;
begin_i := i;
end
else
begin
if p^ = end_Char then
begin
if begin_p <> nil then
begin
inc(begin_p); //跳过 begin_Char
ReciveData_bufflen := i - begin_i - 1; //去掉 begin_Char 和 end_Char
ProcReciveData(begin_p, ReciveData_bufflen, ReciveData);
//解析文本 *000000,00,00,155705,A,2202.12,11307.001,10,0,211209#
Proc(obj, ReciveData);
Result := i; //返回值是 处理的字节数
begin_p := nil;
end;
end;
end;
inc(p);
end;
end;
回调函数Proc是
procedure TFrm_dataInserter.ReciveData(const obj: Pointer;
const ReciveData: TReciveData);
var
ms: TMemoryStream;
begin
//处理报警信息。。。
ms := TMemoryStream(obj);
ms.Write(ReciveData, sizeof(ReciveData));
end;
这个函数没有使用字符串。速度比原来的快了很多,而且基本不占用内存。其实,很久以前用C语言时就这样写了,这样写也是一种怀旧吧。字符串相加实际上是一个比较讨厌的事情,但是你写代码时要经常遇到。一般的情况下,我也是直接相加。只有对速度有特殊要求是我才会用其他写法。这个主要是因为字符串相加时需要重新分配内存,这就会影响速度。顺便说一下,我现在用C#,C#的Stringbuilder就是为了干这个事的,不过他比较凶狠,当实际的内容超出内存空间,他往往分配2倍大小内存。这一点也验证了“要想快,开内存”(貌似有“要想富,圈地皮”的嫌疑)的思想。在处理大量文本时,我的想法是,尽量用PChar来减少字符串传递,不过要慎重。一般情况下没必要这样写。我们在来看看ProcReciveData。这个函数是提取文本里的具体数据的,这里我一样没有用string,基本的写法和ReadReciveData相似,不过这里用了一个自己写的函数fromHex。
procedure ProcReciveData(buf: pchar; len: integer; var ReciveData: TReciveData);
var
p, begin_p: PChar;
i, dataIdx, begin_i, data_len: integer;
AYear, AMonth, ADay, AHour, AMinute, ASecond: Word;
dt: TDateTime;
tmp_p: array[0..16] of char;
begin
p := buf;
begin_p := p;
begin_i := -1;
dataIdx := 0;
for i := 1 to len do
begin
if (p^ = Separator_Char) or (i = len) then // Separator_Char: char = ',';
begin
data_len := i - begin_i;
case dataIdx of
0: Move(begin_p[0], ReciveData.MID, MID_len); //车ID(MID,6个字节)
1: Move(begin_p[0], ReciveData.CMD, CMD_len); //命令标示(CMD,2个字节)
2: ReciveData.TrackData.status := fromHex(begin_p, 2); //状态标示(超速报警,防盗报警。。。)(F,2个字节)
3:
begin //155705:卫星时间(时分秒)15:57:05(6个字节)
AHour := from10(begin_p, 2);
inc(begin_p, 2);
AMinute := from10(begin_p, 2);
inc(begin_p, 2);
ASecond := from10(begin_p, 2);
end;
4: ReciveData.TrackData.valid := begin_p^; // A:数据是否有效(A有效,V无效)(1个字节)
5:
begin //维度
Move(begin_p[0], tmp_p[0], data_len);
tmp_p[data_len] := #0;
TextToFloat(tmp_p, ReciveData.TrackData.location.Dimension, fvCurrency)
end;
6:
begin //经度
Move(begin_p[0], tmp_p[0], data_len);
tmp_p[data_len] := #0;
TextToFloat(tmp_p, ReciveData.TrackData.location.Longitude, fvCurrency)
end;
7: ReciveData.TrackData.speed := from10(begin_p, data_len); //速度
8: ReciveData.TrackData.Direction := from10(begin_p, data_len); //方向(0-360)
9:
begin //211209:卫星时间(日月年)09-12-21 (6个字节)
ADay := from10(begin_p, 2);
inc(begin_p, 2);
AMonth := from10(begin_p, 2);
inc(begin_p, 2);
AYear := from10(begin_p, 2) + 2000;
end;
end;
inc(dataIdx);
begin_i := -1;
end
else
begin
if begin_i < 0 then
if p^ <> ' ' then
begin
begin_p := p;
begin_i := i;
end;
end;
inc(p);
end;
if not TryEncodeDateTime(AYear, AMonth, ADay, AHour, AMinute, ASecond, 0, ReciveData.TrackData.SatelliteTime) then
ReciveData.TrackData.SatelliteTime := now;
end;
fromHex这个函数实际上就是strtoint('$'+'FF'),只是速度快一点而已。这个函数实际上就是一个查表法的计算。其实这里没必要用这个写法,因为这里用fromHex还是用strtoint 对于整个项目而言占的比重太小了。
var
Hex_Table: array[#0..#$FF] of Byte;
procedure init_Hex_Table;
var
i: integer;
ch: Char;
begin
for ch := #0 to #$FF do
Hex_Table[ch] := 0;
for ch := '0' to '9' do
Hex_Table[ch] := StrToInt('$' + ch);
for ch := 'a' to 'f' do
Hex_Table[ch] := StrToInt('$' + ch);
for ch := 'A' to 'F' do
Hex_Table[ch] := StrToInt('$' + ch);
end;
function fromHex(v: PChar; len: integer): Byte;
var
i: integer;
p: PChar;
begin
p := v;
Result := 0;
for i := 1 to len do
begin
Result := Result * 16 + Hex_Table[p^];
inc(p);
end;
{
这种写法 只是 为了追求速度 2009-12-24
比 StrToInt 快40%
比 StrToInt('$'+s) 快20倍 左右
}
end;
进程加载后要调用init_Hex_Table,初始化Hex_Table,代码如下
initialization
begin
init_Hex_Table;
end;
end.
这样的写法以前在单片机上经常见到,有些比较追求速度的计算也用查表法,例如CRC校验的计算。说白了就是预计算和缓存结果。有时候查表法确实是不错的选择。到这里基本就对解析数据优化完了,单以解析数据而言,修改后的代码比原来的代码快不少具体多少我没测试,至少快5倍以上吧。因为解析这些文本文件,在整个流程中占得比重很小,所以我就随便改改了。