被网上小将们骂怕了。但是该写还是要写,而且坚持原创继续毁人不倦。我们的口号是(没有蛀牙!!!)
本垃圾文章描述了一种类似于缓存的查询方式,将数据放入内存中进行查询,从而达到提高响应速度增加吞吐量的目的。方法简单,属于原创。缺点等待大家来评论。
本文技术内容其实很简单,但是我前边会写一些技术之外的东西,因为不仅在中国就是在世界上很多东西比技术更重要。毕竟技术是为了更好的提供服务。往往是最末的一环,请各位程序员和准程序员清醒这一点。另外还简单介绍了设计的过程欢迎有同样小型项目管理经验的同道评论,也给初学者参考。
正文开始:
之前我们参考了南方兄弟招办已有系统(运行过一年,我们本来想去年搞,但是没搞起来,人家先做了)地运行情况。算是学习了经验。最后我们认为整个系统的压力应当集中在填报志愿过程中(废话)。
填报方案选型过程:
一、如果考生直接选择院校名称和专业的话,对服务器和网络压力太大,该方案淘汰。(在后期征集志愿时由于院校数目少可以考虑)
二、志愿翻译:简单说来就是:考生输入报考院校的学校代号和专业代号,然后系统翻译为现实的名称,考生检查是否正确即可。
例如10003:01,02 翻译为:清华大学:工程力学与航天航空工程,艺术设计学。
选择:方案二,考生填写代码,系统翻译志愿。
设计实现过程
设计目标:
翻译过程很简单但是主要是次数比较多。每位考生一次填写正确需要翻译9批次*2-5院校*1院校名称+6专业名称=250次左右的翻译。共55万考生。一共需要翻译1.25亿次。平均到2天的工作时间16小时,平均每秒翻译2170次。再用正态分布估算。我们觉得每秒1万次翻译作为设计目标应当能够满足实际应用的情况。
说起来简单,实际上每个学校翻译的时候还需要判断科类、考生类别、限报条件等,所以sql查询语句效率不会太高。
翻译方案选型:
一、Sql存储过程
已有系统的查询使用存储过程来完成查询,将web服务器和数据库服务器之间的通信减到最小。然后通过对数据库进行优化将性能最大化。
这种方法比较成熟,也是大家首先能够想到的,实施起来风险也较小,数据流量也可很容易的分布到多个数据库上(不要给我提什么群集,挖掘一类的,我们有2台F5,根本不需要那么麻烦再烂的系统也能够运行的差不多。但是不能那样做事。第一设备再先进,也要你的底层架构设计合理才可以。第二、设备技术再先进是人家微软、IBM的又不是自己的,要想进步,要自己去努力。我实在不明白为什么有些人使用起那些名词来那么理直气壮好像是自己发明的一般。)
为了规避风险我们首先实现了该方法,但是并没有在数据优化时下功夫因为我们认为,这种方法效率不高。比如:我们仅仅是只读的查询。一共的3万记录进行1.25亿次查询相当于每条平均查重复4000多次,这是巨大的性能浪费。
而且sql查询时作负载均衡我们嫌麻烦,我们这时就是想要试验去做一种没有试过的高效的内存查询算法。看看到底能要出多少性能来
二、专有数据库查询:实际上无论是平易近人的mssql还是走下神坛的ORCAL由于太大,求全涉及到方方面面。他的数据查询效率并不如专有数据库效率高。就像CE不如PALM快,虽然PALM不先进但是运行快。但是时间长了,由于CE的通用性和兼容性还是占据了比较大的市场。
但是专有的数据库用起来一方面技术比较冷门,另外我们没有时间去进行评估也就无法去选择。单独为一个系统去学习一个冷门数据库不划算。
三、内存数据库,目前内存数据库上网查了查有一个韩国的厂商在工控领域做的不错。另外好像微软的Microsoft SQL Server 2005 Compact Edition
(这个不确认是内存的,但是好像支持,而且好像对多用户支持不好或不支持)做的也不错。感兴趣的去这个地址:http://www.microsoft.com/downloads/details.aspx?FamilyId=E6BC81E8-175B-46EA-86A0-C9DACAA84C85&displaylang=en#filelist
四、私有算法。我们最后选择了自动开发一种查询算法。因为:
1、 创新的需求。做东西技术人员总要有所追求创新,有个东西是你自己的,总是用别人的有啥意思?
2、 我们的查询比较简单,没有必要实现到数据库的层次。为了我们简单的查询,没有必要去实现Codd十二法则?这样我们就可以借着这个项目研究新技术,同时还可以控制风险。
3、 时间充裕:我们设计完之后,用2天就实现了基于内存查询的并进行了评估(虽然网上没有现成的,但是只要想到了确实很简单,就和原来我们写汉字字库实际上一样),结论是值得一试。
具体实现:(请注意本文的实现与现实严格关联,因为实际工作中这些政策年年变,不可能复用,所以也就没有完全面向对象,而只是抽了出来做成类方便维护而已。)
系统具体实现部分
系统实现的原理:
原理很简单将数据放入数组或哈希表中,然后查找即可,关键是如何实现。
关键在于:
第一、 如何将表放入内存中。
第二、 如何对外提供查询。
对于第一个问题我们可以很简单的通过将数组和哈希定义为静态变量从而常驻内存。
请原谅我的代码中有拼音因为。。。教育部的表就是拼音缩写的,因为广大招办有很多老同志,都换成英文不可能。
public static Hashtable htzys = new Hashtable();
然后在构造函数中填充数据例如:
jihuas dsjihua = new jihuas();
jihuasTableAdapters.tjhyxTableAdapter jhapt = new jihuasTableAdapters.tjhyxTableAdapter();
//将指定院校(YXBM从01开始排)放入数组
foreach(jihuas.tjhyxRow dr in dsjihua.tjhyx)
{
//填充数据
}
{
// 在应用程序启动时运行的代码
cache ap = new cache();//加载全局类
}
对于第二个问题,就好说多了,有了数据,查询、就是了。。
好了不说废话,看程序。。
系统代码实现:
///1、构建全局缓存,在全局缓存中构建键列,供志愿查找使用
///2、提供刷新全局缓存功能。(调用构造函数)
///构建全局缓存,应用一次构造函数即可
///提供的全局缓存包括
///1、志愿:院校、专业名称
///2、区域对照表:地市、县区、报名点代码对照表
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Collections;
using System.Text;
using System.Text.RegularExpressions;
/// <summary>
/// cachetest 的摘要说明
/// </summary>
public class cache
{
//str??? 代表字符串数组 ht???代表哈希表
//public static ArrayList alyxs=new ArrayList ();//测试中使用过的方式
public static string[,] stryxs = new string[3200, 4];
///前边是学校数,后边是每组结构 学校名称+kldm+pc+zsfw共4项,
///其中学校编码为从0001开始的4位编码,正好作为数组的下表stryxs[1]=0001院校的数据
public static Hashtable htzys = new Hashtable();///志愿表
public static Hashtable htdishi = new Hashtable();///地市
public static Hashtable htxq = new Hashtable();///县区
public static Hashtable htbmd = new Hashtable();///报名点
/// <summary>
/// 构造函数,加载缓存
/// </summary>
public cache()
{
///首先加载院校代码表
jihuas dsjihua = new jihuas();
jihuasTableAdapters.tjhyxTableAdapter jhapt = new jihuasTableAdapters.tjhyxTableAdapter();
jhapt.Fill(dsjihua.tjhyx);
///没有0院校,需要处理一下
stryxs[0, 0] = "院校填写错误";
stryxs[0, 1] = "";
//将指定院校(YXBM从01开始排)放入数组
foreach(jihuas.tjhyxRow dr in dsjihua.tjhyx)
{
int yxbm = Convert.ToInt16(dr.YXBM);
///07年有一个院校 编号为9001,将其转化为3199,后来临时加的,
///各部门没有协调好以至于编号从9001开始,其实要是使用3200以内闲置的代码号就不用这样处理了
///再有新增的也这样处理。
if (yxbm == 9001)
{
yxbm = 3199;
}
stryxs[yxbm, 0] = dr.YXMC.Trim();
//累加该院校的kldm
//此处没有判断科类是否会重复,不需要判断,重复不影响例如AABBCCDD,只要有即可。此处可以改用STRINGBULIDER,
string kldm = stryxs[yxbm, 1];
string newkldm=dr.KLDM.Trim();
kldm = kldm + newkldm;
stryxs[yxbm, 1] = kldm;
//累加该院校的pc省略
//记录招生范围省略
}
//加载专业代码表
///SELECT DISTINCT yxbm, yxmc, jhxz, kldm, pc, cc
///ORDER BY yxbm
jihuasTableAdapters.tjhzyTableAdapter zyapt = new jihuasTableAdapters.tjhzyTableAdapter();
zyapt.Fill(dsjihua.tjhzy);
foreach (jihuas.tjhzyRow dr in dsjihua.tjhzy)
{
///追加的hash表格式为 keys 专业名称
///其中keys 为 yxbm+ZYBM+kldm+pc
///使用keys查找
htzys.Add(dr.YXBM.Trim() + dr.ZYBM.Trim()+dr.KLDM.Trim()+dr.PC.Trim(), dr.ZYMC);
}
///SELECT YXBM, ZYBM, zymc, jhxz, KLDM, pc, cc
///ORDER BY jhxz, KLDM, pc, cc, YXBM, ZYBM
//加载报名点代码
jihuasTableAdapters.baomingdianTableAdapter bmdapt = new jihuasTableAdapters.baomingdianTableAdapter();
bmdapt.Fill(dsjihua.baomingdian);
foreach (jihuas.baomingdianRow dr in dsjihua.baomingdian)
{
///追加的hash表格式为 keys 报名点名称
///其中keys 为 dmddm
htbmd.Add(dr.ZXDM.ToString().Substring(1,6), dr.ZXMC.ToString().Trim());
}
//加载区县代码
jihuasTableAdapters.td_xqdmTableAdapter xqdapt = new jihuasTableAdapters.td_xqdmTableAdapter();
xqdapt.Fill(dsjihua.td_xqdm);
foreach (jihuas.td_xqdmRow dr in dsjihua.td_xqdm)
{
///追加的hash表格式为 keys 县区名称
///其中keys 为 xqdm
htxq.Add(dr.xqdm.ToString().Substring(1,4), dr.xqmc.ToString().Trim());
}
//加载地市代码
jihuasTableAdapters.td_dsdmTableAdapter dsdapt = new jihuasTableAdapters.td_dsdmTableAdapter();
dsdapt.Fill(dsjihua.td_dsdm);
foreach (jihuas.td_dsdmRow dr in dsjihua.td_dsdm)
{
///追加的hash表格式为 keys 地市名称
///其中keys 为 dsdm
htdishi.Add(dr.dsdm.ToString().Substring(1, 2), dr.dsmc.ToString().Trim());
}
}
//将数据表读入缓存中。然后使用
/// <summary>
/// 志愿转换
/// </summary>
/// <param name="zyxx">志愿信息</param>
/// 序号 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/// 有效 yxdh zydh1 zydh2 zydh3 zydh4 zydh5 zydh6 tj laiyuan ksh pcdm zyh zyhanyi tiaoji validstr bmddm kldm
/// <returns></returns>
public static string tranzhiyuan(ArrayList zyxx)
{
///翻译步骤
///1、检查翻译合法性,是否能够找到(还未考虑)
///2、翻译院校名称
///3、翻译专业名称
///4、翻译调剂
///
//检查cache是否存在
if (!cache.testcache())
{
return "错误!请联系系统管理员,重新启动应用程序以加载系统缓存!";
}
StringBuilder retstr = new StringBuilder();
retstr.Append("");
//首先判断院校编号是否填写,并且不越界
//07年有一个院校编号为9001放入了3199需要处理与前边呼应,被临时变动打乱了
if (zyxx[0].ToString().Trim() == "9001")
{
zyxx[0]= "3199";
}
if (zyxx[0].ToString().Trim() == "")
{
retstr.Append("未填写志愿!");
return retstr.ToString();
}
if (!cache.IsNumeric(zyxx[0].ToString().Trim()))
{
retstr.Append("错误输入!必须是数字!");
return retstr.ToString();
}
if (zyxx[0].ToString().Trim().Length != 4)
{
retstr.Append("错误输入!院校是4位数字!");
return retstr.ToString();
}
if ((Convert.ToInt16(zyxx[0].ToString().Trim()) < 3200))
{
//首先需要取出院校编码,判断院校编码的范围,可能超出,如果超出,则返回院校代码错误,
//然后判断该院校是否有所填科类,返回院校没有符合条件的科类
int yxdh = Convert.ToInt16(zyxx[0].ToString().Trim());
string yxmc = stryxs[yxdh, 0].Trim();
//处理错误院校,该错误为院校编号不连续,用户可能恰巧输入错误的编号,错误编号返回值应为null。另外长度也需要检查
if (yxmc != null)
{
if (yxmc.Length < 2)
{
retstr.Append("无此院校,必须是招生计划中的正确院校编号");
return retstr.ToString();
}
}
else
{
retstr.Append("无此院校,必须是招生计划中的正确院校编号");
return retstr.ToString();
}
//已有的院校,需要判断pc和KLDM
string pcs = stryxs[yxdh, 2].Trim();
string kldms = stryxs[yxdh, 1].Trim();
if (pcs.IndexOf(zyxx[10].ToString().Trim()) == -1)
{
retstr.Append("无此院校,批次错");
return retstr.ToString();
}
if (kldms.IndexOf(zyxx[16].ToString().Trim()) == -1)
{
retstr.Append("无此院校,报考科类错");
return retstr.ToString();
}
//判断招生范围 专业术语部分不用考虑含义
if ((stryxs[yxdh, 3].Trim() != "0") && (stryxs[yxdh, 3].Trim() != "81") && (stryxs[yxdh, 3].Trim() != "82")&&(stryxs[yxdh, 3].Trim() != "83"))
{
if (stryxs[yxdh, 3].ToString().Replace("0","").Trim()!="")//有时可能会有多个0,所以不能REMOVE
{
if (zyxx[9].ToString().Trim().Substring(4, 2) != stryxs[yxdh, 3].Trim())
{
retstr.Append("无此院校,请检查该院校招生范围");
return retstr.ToString();
}
}
}
//都正确返回院校名称
retstr.Append(zyxx[0].ToString() + stryxs[yxdh, 0].Trim() + " ");
///***************************************************
///***************************************************
/// 下边的进程延迟语句发布时一定要去掉。在测试Ajax刷新效果时使用
///***************************************************
//////***************************************************
//System.Threading.Thread.Sleep(1000);//延迟??毫秒再返回
//开始翻译专业
for (int i = 1; i < 7; i++)
{//翻译专业:
//首先判断是否为空
if (zyxx[i].ToString().Trim() != string.Empty)
{
//首先生成keys
///追加的hash表格式为 keys 专业名称
///其中keys 为 YXBM+ZYBM+kldm+pc
string key = zyxx[0].ToString().Trim() + zyxx[i].ToString().Trim() + zyxx[16].ToString().Trim() + zyxx[10].ToString().Trim();
if (htzys.Contains(key))
{
retstr.Append(zyxx[i].ToString() + htzys[key].ToString().Trim() + " ");
}
else
{//如果专业找不到
retstr.Append(zyxx[i].ToString().Trim() + "(无此专业)" + " ");
}
}
else
{//此处else分支为只要有空白不再向下翻译//已经修改为空白也继续翻译,但是需要注意如果后边还有数据才翻译,如果后边的都为空则不再翻译
//if (zyxx[7].ToString() == "1")
//{ retstr.Append("服从分配"); }
//else { retstr.Append("不服从分配"); }
//return retstr.ToString();
bool transflag = false;
for (int j = i; j < 7; j++)//判断该空位专业后边是否还有专业,有则翻译为空白专业,没有则本条为最后一条空白专业,不需要翻译
{
if (zyxx[j].ToString().Trim() != string.Empty)
{
transflag = true;
}
}
if (transflag)
{ retstr.Append(zyxx[i].ToString() + "空白专业" + " "); }
}
}
//最后附加是否服从分配
if (zyxx[7].ToString() == "1")
{ retstr.Append("服从专业调剂"); }
else { retstr.Append("不服从专业调剂"); }
return retstr.ToString();
}
else
{
retstr.Append("无此院校,必须是招生计划中的正确院校编号!");
return retstr.ToString();
}
// return retstr.ToString();
}
/// <summary>
/// 测试当前系统缓存是否存在
/// </summary>
/// <returns></returns>
private static bool testcache()
{
//仅仅是测试了一个表是否存在而已,要有都有,要没都没,这里没有用try因为每次查询try系统消耗太大。而且如果表不存在,静态方法也就不存在了。
if (htzys.Count > 0)
{ return true;}
else { return false; }
}
//从学院字符串数组中取值,调试用
public static string getyxs(int i)
{
return stryxs[i, 0] + stryxs[i, 1];
}
/// <summary>
/// 翻译地市代码
/// 传入字符串代码 返回结果
/// </summary>
/// <param name="dm"></param>
/// <returns></returns>
public static string transdsdm(string key)
{
if (htdishi.Contains(key))
{
return htdishi[key].ToString().Trim();
}
else
{//如果专业找不到
return "地市代码错误!";
}
}
/// <summary>
/// 翻译县区代码
/// 传入字符串代码 返回结果
/// </summary>
/// <param name="dm"></param>
/// <returns></returns>
public static string transxqdm(string key)
{
if (htxq.Contains(key))
{
return htxq[key].ToString().Trim();
}
else
{//如果专业找不到
return "县区代码错误!";
}
}
/// <summary>
/// 翻译报名点代码
/// 传入字符串代码 返回结果
/// </summary>
/// <param name="dm"></param>
/// <returns></returns>
public static string transbmddm(string key)
{
if (htbmd.Contains(key))
{
return htbmd[key].ToString().Trim();
}
else
{//如果专业找不到
return "报名点代码错误!";
}
}
/// <summary>
/// 翻译院校代码
/// 传入字符串代码 返回结果
/// </summary>
/// <param name="dm"></param>
/// <returns></returns>
///public static string transyxdh(string key,string ksh,string pc)
public static string transyxdh(string hanyi)
{
///现在不再翻译,直接取出含义字段中的翻译
///过来的含义字段有2种格式,
///正确的10094河北师范大学
///错误的,无此院校,或报考。。。错
string[] words=hanyi.Split(new char[]{' '});
if (words.Length >0)
{
//还需要判断前4个字符是否是汉字。因为有可能只填写了学校。
string temp = words[0].ToString().Trim();
if(IsNumeric(temp.Substring(0,4)))
{
return temp.Substring(4, temp.Length - 4);
}
return words[0].ToString().Trim();
}
else
{
return "院校代码格式未知!";
}
}
/// <summary>
/// 判断是否是数字
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
private static bool IsNumeric(string str)
{
return Regex.IsMatch(str, @"^-?\d+(\.\d)?$");
}
/// <summary>
/// 将院校表输出测试用
/// </summary>
public static string test()
{
StringBuilder strs = new StringBuilder();
for (int i = 0; i < 3200; i++)
{
string temp="";
try
{
temp = stryxs[i, 3].Trim();
}
catch
{ }
if (temp.Length != 2)
{
strs.Append(temp + "<br />");
}
}
return strs.ToString();
}
}
调用方式说明:传入的为一个ARRAYLIST 00001,01,02。。。。
tbxxdm.Text = zyxx[0].ToString();
tbzy1.Text = zyxx[1].ToString();
tbzy2.Text = zyxx[2].ToString();
tbzy3.Text = zyxx[3].ToString();
tbzy4.Text = zyxx[4].ToString();
tbzy5.Text = zyxx[5].ToString();
tbzy6.Text = zyxx[6].ToString();
终于完了,能够看到这里的都是高手(没有耐心成不了高手)。其实给出这么长的代码段并没有必要,但是如果修改太多又需要时间太多,而且其中有一个修改的地方能够很好的诠释一些不在计划中的意外的处理。所以就直接搬上来了。
原理很简单,实现也不麻烦。
实际效果很好,能够实现每秒百万次级别的查询。而且仅仅加载时访问数据库。我想这可能是这种特殊环境下的最高效率了。
原因很简单:
第一、翻译院校名称根本就没有查找过程,直接下标访问的。
第二、翻译专业名称时哈希表的算法复杂度与直接下标访问差不多(多一次计算hash值)
希望有人能够告诉我有没有更好更快得方法。我很感兴趣。
我这里有负载评测本方法与数据库查询方法比较的测试数据(不使用存储过程的未经优化的数据库同样查询每秒300-1000次,性能保守快2个数量级以上。),改天整理一下再放上来。
反思:
1、 实现的缺陷,静态方法支持多线程吗?我不大好说,但是负载测试时确实4个cpu都跑起来了。盼望高人指点。
2、 该方法占用内存。我们加载了全部3万条记录后,大概占用60m左右内存。当然对于现在我们动辄4g以上的内存不算什么。
3、 不支持sql标准查询。
4、 如果海量数据,内存盛不下如何操作?还有如何支持数据更新?我目前没事的时候在研究这个。希望在用户和数据库中增加一个中间层作缓冲,从而进一步提高数据库吞吐量。作为高考数据量可能没有这个必要,但是对性能的需求是无止境的。
5、 该方法使用起来可靠吗?实际使用时没有出现问题,不知道有没有高手指出可靠性的缺陷。
6、有没有人能够比较与.net新增的缓存的比较。