遇到一个功能需求,要求给不同终端下发资源中心存储的资料,例如资料数100万条,终端数1000台,现需定时给每个终端推送一条资料,要求推送的资料为随机获取且不允许被重复推送给终端。
方案1:通过查询资料库的记录与终端的历史推送记录,得到该终端还未使用过的资料列表,最后找随机位置上的记录作为本次推送的结果。伪代码实现过程如下:
//第1步,查询数据库,获取可用资料
-- SQL脚本,资料表document,历史记录表history, 资料主键qid,资料数据内容content,设备编号receiver,测试设备的编号值11101
Select qid,content from document t where not exists(select 1 from history t1 where t1.receiver=11101 and t.qid=t1.qid)
//第2步,随机取一个数,dicDocument为可用资料字典
Random r=new Random();
Var documentIndex= r.Next(0, dicDocument.Count-1);//资料的编号
Var documentContent = dicDocument[documentIndex].Value;//返回资料内容
本方案不建议使用,第1步执行过程涉及到全表扫描操作,执行效率并不高,同时终端数多一些的话对数据库访问频次也会变多,数据库本身的压力也会较大。因此,效率+资源开销这两指标都比较一般,且当数据量较大时,大量的此类查询可能会导致数据库服务不可用。
方案2:录入资料时生成终端的待推送任务队列,由推送任务自行为各个终端从任务队列中取最前面的记录作为推送内容。
本方案不建议使用,会提前占用较多的硬盘存储空间,重要的是新增一条资料则需生成多条任务执行计划,写库的内容较多,同理删除一条资料则需将所有终端的此资料推送任务清除。综合原因则是:1、会提前占用更多的硬盘资料存储空间;2、对资料进行维护时涉及到是数据更新数较多,同样也会使用更多的数据库资源;3、终端的管理约束并不是固定的,可能某些设备会被回收或不再进行集中管控。因此,本方案2虽整体优于方案1,但还不是比较合适的实现过程。
方案3:使用bit位标记的思路,结果以二进制文件的形式存储在硬盘。
1,内容描述:
l 包括了全局唯一一份资料文件,存储在硬盘,系统启动后自动加载进内存,支持线程安全。
l 终端各自保存一份历史推送记录文件,存储在硬盘,给终端执行推送任务时从硬盘加载到内存中。
l 单次推送任务中,终端只会被推送一次,如果失败也不做重试过程(此功能暂无实现需求)。
l 终端的推送任务结束后,数据库先插入一条历史记录,然后回写资料推送统计计数,最后同步终端历史推送记录文件。此过程需使用数据库事务保证结果记录一致性,且写总段历史推送记录文件的步骤放到最后,当硬盘操作正常完成时事务提交,否则进行回滚终端推送任务执行结果的操作过程。
l 资料记录的编号是连续的,从1开始。
l 资料记录没有物理删除,只进行逻辑删除。
l 新增资料时,需同步修改全局资料文件内容。
l 删除资料时,需同步将全局资料文件中对应的记录进行相关标记。
l 编辑资料时,无需影响全局资料文件。
2,实现过程
2.1 数据结构定义伪代码,实际类型并不存在
/*2.1.1 定义全局资料文件记录格式,并初始化加载*/
Public static Bit[0] aryBitMain;//bit数组,长度为8的倍数,如0,8,16,24...
bytesAry= fiels.readbytes(@“d:\sequence_data\main.dic”);//将文件流作为字节数组
aryBitMain = (bit[])bytesAry;//转换后得到bit数组
/*2.1.2新增一个资料的方法,例如资料编号字段qid是9,此qid是连续的,最后一个 新增的qid肯定是最大的并且等于前一个资料的qid+1 */
Lock(aryBitMain)//加锁,阻塞其它线程读写aryBitMain,例如其它管理员变更资料库。
{
//判断aryBitMain的长度是否大于等于qid
If (aryBitMain.Length>=qid)
{
aryBitMain[qid-1] = 1;//qid数值对应的索引位置上的值变成1(默认为0)
Return;
}
//增加aryBitMain的长度
aryBitMain.Length = qid;//将数组长度设置为qid,当前示例中实际长度为16
aryBitMain[qid-1] = 1;//第9个元素的值为1,前面8个元素值不变,后7个的值为值0
//保存aryBitMain数组为本地二进制文件
Files.writeallbytes(@“d:\sequence_data\main.dic”);
}
/*2.1.3删除一个资料,例如删除编号为9的资料*/
Lock(aryBitMain)//加锁,阻塞其它线程读写aryBitMain,例如其它管理员变更资料库。
{
If (qid>aryBitMain) return;//极端情况,qid并不存在,仅为了防止报错
aryBitMain[qid-1] = 0;//将qid对应位置的元素值标记为已失效,例如qid=9时,全局资料文件记录的第9个bit位的值被设置为0。
//保存aryBitMain数组为本地二进制文件
Files.writeallbytes(@“d:\sequence_data\main.dic”);
}
/*2.1.4给指定终端提取待推送资料,例如终端编号10001*/
Lock(aryBitMain)//加锁,阻塞其它线程读写aryBitMain,例如其它管理员变更资料库。
{
//读取终端的历史推送记录文件到内存
Var bytesAry= files.read(@”d:\sequence_data\10001.rec”);//读到字节数组
Var usrQidRecord = (int[])bytesAry;//转为int数组(模拟)
Var isHitOK=false;//是否命中
Var retryCount = 100;//最多尝试100次
Var randValue = 0;//随机数
While(!isHintOK && retryCount>0)
{
retryCount --;
//获取随机位置(题号)
randValue = new Random().Next(1, aryBitMain.length);
//题号内容还未失效 && 终端的历史推送记录中没有用过该题号
If (aryBitMain[randValue-1]==1 && ! usrQidRecord .Contains(randValue ))
{
isHitOK = true;//命中了,无需继续尝试命中题号
Break;
}
}
If (isHitOK)//已命中则直接返回题号
{
Return randValue;
}
Else//重试了100次都没命中,则随机取一个未生效的题号,此过程可重试30次
{
isHitOK=false;
retryCount=30;
While(retryCount>0)
{
retryCount --;
randValue = new Random().Next(1, aryBitMain.Length);
If (aryBitMain[randValue-1]==1)//只要文档没有失效则命中,几乎都会被重复推送的
{
Return randValue;
}
}
}
}
/*2.1.5执行推送流程,例如终端编号10001*/
callSendMessage(dynamic obj)
{
//获取可推送资料的编号,例如编号为9
//执行2.1.4过程
qid=9;//资料编号
//查数据库,获取编号对应的资料内容,按主键查询的速度很快
Sql = “select content from document where qid=9”;
Var content = sql.value[“content”].ToString();
//执行发送流程,耗时较多约1-3秒
Var emailState = _sendemail(obj.email);
If (emailstate=0)//发送失败
{
Return;
}
//开启数据库事务
Dbtran.start();
//写历史记录,新增数据的速度也比较快
Sql = “insert into document_history”;
//回写统计表,按主键修改行级非索引字段,执行很快
Sql = “update document set successcount+=1 where qid=9”;
//回写用户发送记录到硬盘,步骤2.1.4中有从硬盘读取过文件到内存,无需重读
usrQidRecord.length +=1;
usrQidRecord[usrQidRecord.length-1] = qid;
//保存usrQidRecord数组为本地二进制文件
Files.writeallbytes(@“d:\sequence_data\10001.rec”);
Dbtran.commit();
}
本方案是实现过程避免了执行复杂sql频繁从数据库中读取结果的操作,减少了对数据库资源的不必要占用。利用bit位来映射编号对应资料的状态,不仅在本需求中能满足使用要求,也能有力地提升空间使用效率,同时执行的时间复杂度上也有较好的表现。假设当前资料库中有1亿条记录,那么仅需要12M的内存和硬盘空间即可将资料数据的对应映射关系进行存储、读写和运算,同时仅需维护一份文件,该资料文件可常驻内存,避免反复从硬盘加载到内存的过程,节省了准备计算资源的耗时,但是需要做好多线程访问此数据时的同步控制。目前还只支持22亿个资料数据,如需扩展则要增加新的资料库文件,使多个资料库文件并行存在。综上所述,实现需求中的数据提取功能时本方案整体优于方案1和方案2,能在时间和空间复杂度上的表现更好,同时程序可维护性难度中下。