记一次性能优化--利差
背景
利差查询的性能一直是个问题,利用插件如arthas或code request分析耗时,发现耗时都在取数上。

因为利差的过滤条件有很多,要先过滤得到债券的每日利差,再按发行人汇总中位数或平均值得到发行人的单日利差,再汇总发行人的利差得到单日利差。一般都是一次获取一年--三年的利差。
原有方案
所以老的方案是在数据库层面做中位数或平均值计算得到主体利差,再在代码层面做主体利差汇总。
慢就慢在数据库层面,如果不在数据库层面做主体利差计算,则数据量又太大。按筛选条件过滤后的债券一般数量级在50-40000。按2w算,2w x 250 = 500w数据。试了很多优化方案,索引、分表。效果都不好。
接口耗时在十几秒到几分钟之间,还忽快忽慢,这个跟数据库本身有关。有时转圈会一直转,所以体验非常不好。
优化方案
定位到性能瓶颈处,查询了大数据量下的处理方案,比较redis、es等,最终结合产品现状(已在使用redis),选择redis进行缓存中间层。
以【利差类型:价格集:基准曲线:日期】作为hash值,数据结构选择hash,债券code作为字段key,利差,剩余期限作为字段value。
优化后,在取1年的场景下,债券量2w的场景,6-7秒,4w的场景 14秒,本地测试。服务器上理论上会快点。
几百个债的场景,基本都是秒出。
--一般利差
COUPON:EVAL:2142:2025-07-22
COUPON:QUOTE:2142:2025-07-22
--超额列差
EXCESS:EVAL:2025-07-22

核心代码
有一点注意,一次交互跟多次取数交互,耗时会存在数量级的区别。
Pipeline pipe = jedis.pipelined();
...
// 真正向 Redis 发送请求
pipe.sync();
package com.xquant.xcrms.utils;
import com.xquant.xcrms.common.utils.RedisUtils;
import com.xquant.xcrms.concentrate.constant.SpreadMethodConstant;
import com.xquant.xcrms.coupon.model.dto.CouponSingleResultDto;
import com.xquant.xcrms.spread.constant.CreditPricingConstant;
import com.xquant.xcrms.spread.dto.common.BondPeriodDto;
import com.xquant.xcrms.utils.dao.SpreadRedisDao;
import com.xquant.xcrms.utils.model.BondCorpMapDto;
import com.xquant.xcrms.utils.model.BondSpreadDto;
import com.xquant.xcrms.utils.model.SpreadWithCorpCodeDto;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
/**
* 利差缓存操作类
* Description: <br>
* @date 2025/7/22
*/
@Component
public class SpreadRedisUtil {
private RedisUtils redisUtils;
private SpreadRedisDao spreadRedisDao;
public SpreadRedisUtil(RedisUtils redisUtils, SpreadRedisDao spreadRedisDao) {
this.redisUtils = redisUtils;
this.spreadRedisDao = spreadRedisDao;
}
/**
* 测试用
* 缓存一年的国开债一般利差
*
* @param date
*/
public void cacheCouponEval2142InYear(String year) {
String tablename = "TCREDIT_BOND_EVAL_2142_" + year;
List<String> dates = spreadRedisDao.querySpreadRateDate(tablename);
for (int i = 0; i < dates.size(); i++) {
String date = dates.get(i);
cacheCouponEval2142(date);
}
}
/**
* 测试用
* 缓存单个日期的国开债一般利差
*
* @param date
*/
public void cacheCouponEval2142(String date) {
List<BondSpreadDto> list = spreadRedisDao.queryBondSpreadByDate("TCREDIT_BOND_EVAL_2142_2024", date);
Map<String, String> spreadMap = new LinkedHashMap<>();
for (BondSpreadDto dto : list) {
spreadMap.put(dto.getoCode(), dto.getRateSpread() + "," + dto.getRemainY());
}
cacheSpread(date, spreadMap, CreditPricingConstant.COUPON, CreditPricingConstant.EVAL, "2142");
}
/**
* 测试用
* 取数
*
* @param year
* @return
*/
public List<CouponSingleResultDto> loadCouponEval2142(String year) {
String tablename = "TCREDIT_BOND_EVAL_2142_" + year;
//List<String> dates = Arrays.asList("2024-11-01");
List<String> dates = spreadRedisDao.querySpreadRateDate(tablename);
List<BondCorpMapDto> bondList = spreadRedisDao.queryBondCode();
List<String> ocodeList = bondList.stream().map(c -> c.getoCode()).collect(Collectors.toList());
Map<String, String> codeMap = bondList.stream().collect(Collectors.toMap(
item -> item.getoCode(), // 键:code
item -> item.getIssuerCode(), // 值:对象本身
(existing, replacement) -> existing // 合并函数(code唯一时可简单保留第一个)
));
List<CouponSingleResultDto> res = loadSpread(ocodeList, codeMap, dates, CreditPricingConstant.COUPON, CreditPricingConstant.EVAL, "2142", SpreadMethodConstant.MEDIUM, null);
return res;
}
/**
* 缓存利差
*
* @param date
* @param spreadMap KEY: OCODE(债券代码) VALUE: RATE_SPREAD,REMAIN_Y
* @param rateType 一般利差:CreditPricingConstant.COUPON (默认)
* 超额利差:CreditPricingConstant.EXCESS
* @param priceType CreditPricingConstant.EVAL 中债行权估值 (默认)
* CreditPricingConstant.QUOTE 中介报价
* CreditPricingConstant.TRADE 中介成交价格
* @param baseCurveCode 基准曲线: 2142 国开债收益率曲线 1232 国债收益率曲线 1102 AAA估值收益率曲线
*/
public void cacheSpread(String date,
Map<String, String> spreadMap,
String rateType,
String priceType,
String baseCurveCode) {
if (spreadMap == null || spreadMap.isEmpty())
return;
//一般利差 COUPON:EVAL:2142:2025-07-22
//超额列差 EXCESS:EVAL:2142:2025-07-22
String key = rateType + ":" + priceType + ":" + baseCurveCode + ":" + date;
Jedis jedis = redisUtils.getJedisDB0();
try {
// 1) pipeline 批量写入
Pipeline pipe = jedis.pipelined();
// 每 5 000 条一个 pipeline 包,防止请求过大
Map<String, String> batch = new HashMap<>(5000);
for (Map.Entry<String, String> e : spreadMap.entrySet()) {
batch.put(e.getKey(), e.getValue().toString());
if (batch.size() == 5000) {
// 直接覆盖
pipe.hset(key, batch);
batch.clear();
}
}
if (!batch.isEmpty()) {
pipe.hset(key, batch);
}
// // 2) 设置过期 无需过期
// if (ttlSeconds > 0) {
// pipe.expire(key, ttlSeconds);
// }
// 3) 一次网络往返
pipe.sync();
} finally {
jedis.close();
}
}
/**
* 批量取利差
*
* @param bonds 债券代码列表(10 万以内)
* @param codeMap key: 债券code value: 主体code
* @param dates 日期列表(yyyyMMdd,500 天以内)
* @param rateType 一般利差:CreditPricingConstant.COUPON (默认)
* 超额利差:CreditPricingConstant.EXCESS
* @param priceType CreditPricingConstant.EVAL 中债行权估值 (默认)
* CreditPricingConstant.QUOTE 中介报价
* CreditPricingConstant.TRADE 中介成交价格
* @param baseCurveCode 基准曲线: 2142 国开债收益率曲线 1232 国债收益率曲线 1102 AAA估值收益率曲线
* @param spreadMethod 统计方法
* 中位数: SpreadMethodConstant.MEDIUM (默认)
* 平均值: SpreadMethodConstant.AVG
* @param periodDateList 剩余期限过滤限制
* @return Map<date, spread>
* @see com.xquant.xcrms.spread.constant.CreditPricingConstant
* @see com.xquant.xcrms.concentrate.constant.SpreadMethodConstant
*/
public List<CouponSingleResultDto> loadSpread(List<String> bonds,
Map<String, String> codeMap,
List<String> dates,
String rateType,
String priceType,
String baseCurveCode,
String spreadMethod,
List<BondPeriodDto> periodDateList
) {
List<CouponSingleResultDto> resMap = new ArrayList<>();
// 结果容器:外层按日期,内层按债券
Map<String, Map<String, Double>> result = new LinkedHashMap<>(dates.size());
for (String d : dates) {
result.put(d, new HashMap<>(bonds.size()));
}
// 每条 pipeline 一次最多拉 5 000 个 field(经验值)
final int BATCH = 5_000;
Map<String, List<String>> bondBatches = new LinkedHashMap<>();
for (int i = 0; i < bonds.size(); i += BATCH) {
int end = Math.min(i + BATCH, bonds.size());
List<String> batch = bonds.subList(i, end);
bondBatches.put(batch.hashCode() + "", batch);
}
Boolean filterPeriod = false;
if (CollectionUtils.isNotEmpty(periodDateList)) {
filterPeriod = true;
}
Jedis jedis = redisUtils.getJedisDB0();
try {
Pipeline pipe = jedis.pipelined();
// 预创建 Response,避免同步等待
Map<String, Map<String, Response<List<String>>>> resp = new LinkedHashMap<>();
for (String d : dates) {
Map<String, Response<List<String>>> dayRes = new LinkedHashMap<>();
resp.put(d, dayRes);
for (String hashcodeKey : bondBatches.keySet()) {
//一般利差 COUPON:EVAL:2142:2025-07-22
//超额列差 EXCESS:EVAL:2142:2025-07-22
List<String> bb = bondBatches.get(hashcodeKey);
String key = rateType + ":" + priceType + ":" + baseCurveCode + ":" + d;
dayRes.put(key + "|" + hashcodeKey,
pipe.hmget(key, bb.toArray(new String[0])));
}
}
// 真正向 Redis 发送请求
pipe.sync();
// 解析结果 将债券主体Map、中位数or平均值 也传进来 这里直接计算
for (String d : dates) {
List<SpreadWithCorpCodeDto> spreadInday = new ArrayList<>();
Map<String, Response<List<String>>> dayResp = resp.get(d);
for (Map.Entry<String, Response<List<String>>> e : dayResp.entrySet()) {
List<String> batchBonds = bondBatches.get(e.getKey().split("\\|")[1]);
List<String> vals = e.getValue().get();
for (int i = 0; i < vals.size(); i++) {
String v = vals.get(i);
if (v != null) {
String[] vArr = v.split(",");
if (vArr.length < 2) {
continue;
}
//利差值
String s1 = vArr[0];
//剩余期限(行权期限) 时点下
String s2 = vArr[1];
if (filterPeriod && handlePeriodJudge(s2, periodDateList)) {
SpreadWithCorpCodeDto item = new SpreadWithCorpCodeDto();
item.setSpread(Double.parseDouble(s1));
item.setCorpCode(codeMap.get(batchBonds.get(i)));
spreadInday.add(item);
} else {
SpreadWithCorpCodeDto item = new SpreadWithCorpCodeDto();
item.setSpread(Double.parseDouble(s1));
item.setCorpCode(codeMap.get(batchBonds.get(i)));
spreadInday.add(item);
}
}
}
}
if (spreadInday.size() > 0) {
//先按主体分组 取中位数或平均值
Map<String, List<SpreadWithCorpCodeDto>> spreadIndayMap = spreadInday.stream().collect(Collectors.groupingBy(c -> c.getCorpCode()));
List<Double> vWithCorp = new ArrayList<>(spreadIndayMap.size());
for (String corpCode : spreadIndayMap.keySet()) {
List<SpreadWithCorpCodeDto> listInCorp = spreadIndayMap.get(corpCode);
if (SpreadMethodConstant.AVG.equals(spreadMethod)) {
Double v = listInCorp.stream().map(c -> c.getSpread()).mapToDouble(Double::doubleValue)
.average().orElse(Double.NaN);
vWithCorp.add(v);
} else {
//默认中位数
List<Double> sorted = listInCorp.stream().map(c -> c.getSpread()).sorted().collect(Collectors.toList());
int size = sorted.size();
Double v = size % 2 == 1
? sorted.get(size / 2)
: (sorted.get(size / 2 - 1) + sorted.get(size / 2)) / 2.0;
vWithCorp.add(v);
}
}
//将主体的结果汇总成单日的利差
if (SpreadMethodConstant.AVG.equals(spreadMethod)) {
Double v = vWithCorp.stream().mapToDouble(Double::doubleValue)
.average().orElse(Double.NaN);
resMap.add(new CouponSingleResultDto() {{
setRateDate(d);
setRate(new BigDecimal(v * 100));
}});
} else {
//默认中位数
vWithCorp = vWithCorp.stream().sorted().collect(Collectors.toList());
int size = vWithCorp.size();
Double v = size % 2 == 1
? vWithCorp.get(size / 2)
: (vWithCorp.get(size / 2 - 1) + vWithCorp.get(size / 2)) / 2.0;
resMap.add(new CouponSingleResultDto() {{
setRateDate(d);
setRate(new BigDecimal(v * 100));
}});
}
}
}
} finally {
jedis.close();
}
return resMap;
}
/**
* 判断剩余期限是否满足条件
*
* @param maturity
* @param periodDateList
* @return
*/
private Boolean handlePeriodJudge(String maturity, List<BondPeriodDto> periodDateList) {
double v = Double.parseDouble(maturity);
for (int i = 0; i < periodDateList.size(); i++) {
BondPeriodDto period = periodDateList.get(i);
BigDecimal begin = period.getBegin();
BigDecimal end = period.getEnd();
Boolean b1 = false;
Boolean b2 = false;
if (begin == null) {
b1 = true;
} else if (begin != null && v > begin.doubleValue()) {
b1 = true;
}
if (end == null) {
b2 = true;
} else if (end != null && v <= end.doubleValue()) {
b2 = true;
}
if (b1 && b2) {
//有一组条件满足即可
return true;
}
}
return false;
}
}

浙公网安备 33010602011771号