一,需求分析
如上图,app实现基金经理的查询,可根据年化回报率,综合得分,升序或降序排列,可下拉刷新,即实现分页效果。
生产数据24000左右,不算大,但是app查询要求响应速度,不能查询表,可以把数据放到缓存中,查询缓存。
二,背景
基金经理数据是买第三方的数据,每天更新一次,定时调第三方接口拉取数据,落库,写缓存。
拉取节奏,0-9点,过10min拉取。
三,怎么存,选择什么数据结构
ZSET
需要排序,可以选择有序集合zset,很重要的一点是,需要排序的字段,都需要分别缓存,即回报率和综合评分缓存两份数据,相互独立。
ZADD key score member [[score member] [score member] ...]
key:基金的key
sorce:回报率或综合评分
member:基金经理编码
除此之外,还可以通过区间查询实现分页
ZRANGE key start stop [WITHSCORES]
返回有序集 key 中,指定区间内的成员。
其中成员的位置按 score 值递增(从小到大)来排序。
ZREVRANGE key start stop [WITHSCORES]
返回有序集 key 中,指定区间内的成员。
其中成员的位置按 score 值递减(从大到小)来排列。
Hash
到此完成了,排序及分页,但是缺少基金的其他详细信息,姓名,学历,简介。这个数据也要存储,可以选择hash。
HSET key field value
将哈希表 key 中的域 field 的值设为 value 。
如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。
如果域 field 已经存在于哈希表中,旧值将被覆盖。
HGET key field
返回哈希表 key 中给定域 field 的值。
HLEN key
返回哈希表 key 中域的数量
如上,field:基金经理编码 value:基金经理详情。
查询的结果由两部分缓存结果,通过分页查询取排序数据,在通过HGet获取基金详细,数量总量,HLEN取得。
四,代码
1.定时落表,写缓存
@Scheduled(cron = "${managerlist.job.cron:0 10 */9 * * ?}")
public void run() {
String uniqueVal = UUID.randomUUID().toString();
String lockKey = RedisKeyHelper.syncManagerListLockKey();
try {
if (redisMgr.tryLock(lockKey, uniqueVal, LOCK_EXPIRE_TIME_SEC)) {
process();
}
} catch (Exception e) {
log.error("Fetch sync manager list exception", e);
} finally {
redisMgr.releaseLock(lockKey, uniqueVal);
}
//等待1分钟后,执行刷新缓存操作,排序的缓存
try {
long lateTime = 60000;
Thread.sleep(lateTime);//等待1分钟后
//刷入数据
refreshManagerCacheByProportion();//刷入收益排行榜
refreshManagerCacheByScore();//刷入分数排行
} catch (Exception e) {
log.error("refresh manager list order exception", e);
}
}
//刷新基金经理年回报率排行榜缓存
private void refreshManagerCacheByProportion() {
List<Manager> list = service.queryManagerListByProportion();
if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(list)) {
for (Manager v : list) {
String betaCode = v.getBetaCode();
if (StringUtils.isNotBlank(betaCode)) {
Double proportion = v.getReturnProportion();
if (proportion == null) {
proportion = 0.0;
}
redisMgr.hput(RedisKeyHelper.managerAllHInfoKey(), betaCode, JSON.toJSONString(v));
redisMgr.zadd(RedisKeyHelper.managerRankProporKey(), proportion, betaCode);
}
}
}
}
//刷新基金经理综合评分排行榜缓存
private void refreshManagerCacheByScore() {
List<Manager> list = service.queryManagerListByScore();
if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(list)) {
for (Manager v : list) {
String betaCode = v.getBetaCode();
if (StringUtils.isNotBlank(betaCode)) {
Double score = v.getScore();
if (score == null) {
score = 0.0;
}
redisMgr.hput(RedisKeyHelper.managerAllHInfoKey(), betaCode, JSON.toJSONString(v));
redisMgr.zadd(RedisKeyHelper.managerRankScoreKey(), score, betaCode);
}
}
}
}
public void process() {
List<ManagerListDTO> dtos = managerApi.fetchManagerList();
log.info("Need to sync manager list size:{}", dtos.size());
if (CollectionUtils.isEmpty(dtos)) {
log.warn("No data to sync fund codes");
return;
}
service.clearManagerByDay();//首先清除当天历史数据
List<List<ManagerListDTO>> pars = Lists.partition(dtos, BATCH_SIZE);
pars.stream().forEach(l -> {
executor.execute(() -> {
try {
long s = System.currentTimeMillis();
List<Manager> mlist = new ArrayList<>(l.size());
List<List<ManagerListDTO>> m = Lists.partition(l, 100);
m.stream().forEach(r -> mlist.addAll(converManager(r)));
List<ManagerEntity> dataList = dealManager(mlist);
StopWatch stopwatch = new StopWatch();
stopwatch.start("基金经理批量写入es");
esService.saveBatchManager(dataList);
stopwatch.stop();
log.info("BatchSaveManager spend:{}", stopwatch.getTotalTimeMillis());
service.saveManagerList(mlist);
long e = System.currentTimeMillis();
log.info("Take {}s to handle the size#{} data", (e - s) / 1000, l.size());
} catch (Exception e) {
log.error("Sync manager list exception", e);
}
});
});
}
2.查询排行
@Override
public SearchResDTO searchAllManagerByOrder(String sortType, int sort, int page, int size) {
log.info("searchAllManagerByOrder manager{} ,{} ,{}, {}", sortType, sort, page, size);
int start = (page - 1) * size;
String mainKey = null;
if ("score".equals(sortType)) {//按照分数排行
mainKey = RedisKeyHelper.managerRankScoreKey();
} else {//默认按照年化回报率排行
mainKey = RedisKeyHelper.managerRankProporKey();
}
List<String> list = new ArrayList<>();
long end = start + size - 1;
if (sort == -1) {//默认降序
Set<String> abc = redisMgr.zrevrange(mainKey, start, end);
if (abc != null) {
list = new ArrayList<>(abc);
}
} else {//其他升序
Set<String> abc = redisMgr.zrange(mainKey, start, end);
if (abc != null) {
list = new ArrayList<>(abc);
}
}
List<FundManagerDTO> result = new ArrayList();
int count = 0;
if (list != null && list.size() > 0) {
String dictionaryKay = RedisKeyHelper.managerAllHInfoKey();
count = (int) redisMgr.hLen(dictionaryKay);
for (String code : list) {
Object info = redisMgr.hget(dictionaryKay, code);
if (info != null) {
FundManagerDTO node = JSONObject.parseObject(info.toString(), FundManagerDTO.class);
result.add(node);
}
}
}
SearchResDTO searchResDTO = new SearchResDTO();
searchResDTO.setSearchFundManagers(result);
searchResDTO.setCount(count);
return searchResDTO;
}