高并发高可用高可靠性的千人千面项目技术架构分析
项目需求目标及实现方案
1.项目需求,在营销领域不断迭代发展的过程中,如何去挖掘顾客的需求并且去满足顾客的需求,一直是营销领域研究的课题。如何针对不同的客户需求进行个性化营销,众多电商公司、营销平台一直在不断探索和深入研究;这也是算法和大数据商业应用很重要的一个方面。例如淘宝2013提出了千人千面的概念,其实就是推荐算法,其实众多电商公司在这一方面均有应用。我们做的这个项目是营销活动的千人千面,其实就是帮助卖家去更精准的对顾客进行营销,顾客看到的也是自己感兴趣的活动,会有更好的体验。
2.项目目标,我们这个项目的三个目标:
–高并发,tps在30w以上
–高可用性,tp99在10ms内,百分之99的请求需要在10毫秒内完成响应
–高可靠性,这基本是项目的必然要求
3.实现方案,在实现方案上会分如下几个部分去分析。
海量数据存储
为了保证程序的可用性,我们将数据存放在Redis集群中,如果以k/v方式去存储会浪费大量的资源,并且用一般的序列化和反序列化工具也很难满足性能需求。因此我们选择了Google 的一款工具protobuf来作为我们存储和使用数据的一种格式;经过对比测试protobuf比使用fastjson进行序列化之后存储,数据量减少了三分之二,并且反序列化的性能也非常好。protobuf的使用
基于LRU的热点数据缓存
我们这个项目是基于活动的千人千面项目,每个商家都可以设置自己的活动体,每个活动体中有一系列的子活动;因此每个活动体的数据量就比较大,并且活动体的数据也很多,如果每次推荐计算都从Redis取数据的话,在tps30w的情况下网络带宽肯定会被占满。我们的解决办法是缓存一部分的热点活动体,让大部分的推荐计算直接在缓存中取数据。那么问题来了什么样的活动体是热点的活动体呢?当然就是被访问次数多并且最近经常被使用的。我们缓存的数据量肯定是有限的,没有办法把所有的数据都缓存下来,我们需要不断的更新替换热点数据,把相对热的数据缓存,把之前热目前不热的数据暂时移除。我们自己设计过这样的缓存策略,发现有现程的基于LRU(最近最少使用)的缓存工具,我们对比了几款,发现Google 的ConcurrentLinkedHashMap和我当初设计的思路完全吻合,当然Google 的这个工具已经经过验证的肯定比我们自己实现出来的稳定,我们就选取了这个缓存工具。关于我们缓存实现的设计思路和对几款缓存工具的比较后面会再用一个篇幅去阐述。
数据一致性问题
举例说明,商家的活动提数据是可以随时更新的,如果同一个商家的同一个活动体数据同时请求了多个更新,分布式环境下,由于网络等原因可能最后一个更新先执行而之前的更新后执行,那么最后更新的结果将不是最新的数据。怎么解决这个问题呢,加上了一个时间戳,处理请求时通过时间戳来判断这个请求是不是最新的。下面代码部分附上了代码。
实现高性能的千人千面
解决了数据存储问题、基于LRU热点数据缓存的问题,接下来就是实现推荐匹配算法了;这里参考了es的语法,直接在代码部分用代码展示,活动体protobuf模型在代码部分展示。
进一步优化
在从Redis获取多种数据时使用多线程并行执行
日志优化,在最大程度上精简日志
避免服务间跨机房访问
代码展示
解决数据一致性代码展示
.
// 通过时间戳判断是否是最新请求
String key_nx = key+SPLIT+SUBFIX_NX;
String key_stamp = key+SPLIT+SUBFIX_STAMP;
boolean excute = false;
long recordTime = System.currentTimeMillis();
while(!excute) {
if(System.currentTimeMillis()-recordTime>MAX_TIME) {
throw new CommonException("操作超时");
}
if(jimDBUtil.setnx(key_nx, timestamp.toString(), 1l)) {
excute = true;
String stamp = jimDBUtil.get(key_stamp);
if(StringUtils.isNotBlank(stamp) &×tamp<Long.valueOf(stamp)){
throw new CommonException("当前操作已过时");
}
jimDBUtil.set(key_stamp, timestamp.toString());
jimDBUtil.del(key);//得先移除jimDB中的数据
//广播同步缓存中的数据
jmqProducerUtil.send(sync_cache_topic,key,SELLE_RRULE_BUSINESSID);
jimDBUtil.del(key_nx);
}
}
活动体的protobuf模型
.
// 活动体模型
message PSubRuleInfo {
PSellerRule sellerRule = 1;
string content = 2;
string contentType = 3;
uint32 level = 4;
string subId = 5;
uint64 startTime = 6;
uint64 endTime = 7;
}
message PRuleInfo {
uint64 startTime = 1;
uint64 endTime = 2;
repeated PSubRuleInfo sruleList = 3;
}
message PBoo {
map<string, string> term = 1; // 定义Map对象
map<string, string> terms=2;//
map<string, PRangeEntity> range=3;//
repeated PSellerRule ruleList = 4; // repeated 列表
}
message PRangeEntity{
string gte = 1;
string lte = 2;
string gt = 3;
string lt = 4;
}
message PSellerRule {
PBoo must = 1;
PBoo must_not = 2;
PBoo should =3 ;
}
匹配算法
.
// 匹配算法
public boolean excuteRule(PSellerRule rule, Map<String, Object> attriMap) throws Exception {
if(null == rule || null == attriMap || attriMap.size()==0) {
throw new CommonException("rule or attribute is null");
}
PBoo must = rule.getMust();
Map<FieldDescriptor, Object> mustFields = must.getAllFields();
if(null != must && mustFields != null && mustFields.size()>0) {
if(!mustRule(must,attriMap)) {
return false;
}
}
PBoo must_not = rule.getMustNot();
Map<FieldDescriptor, Object> mustNotFields = must_not.getAllFields();
if(null != must_not && mustNotFields != null && mustNotFields.size()>0) {
if(!mustNotRule(must_not,attriMap)) {
return false;
}
}
PBoo should = rule.getShould();
if(null != should) {
if(!shouldRule(should,attriMap)) {
return false;
}
}
List<PSellerRule> ruleList = must.getRuleListList();
if(ruleList != null && ruleList.size()>0) {
for (PSellerRule sellerRule : ruleList) {
if(!excuteRule(sellerRule,attriMap)) {
return false;
}
}
}
List<PSellerRule> ruleList2 = must_not.getRuleListList()