概述
在本周工作中,接到了两个非常有趣的需求。
- 在医疗保险项目中,mysql数据库中某些敏感字段将会被加密,当他人要查询这些数据时,我要将这些数据解密出来,并交个调用者。
- 在网关管理系统中,做一个关于服务响应时间的统计报表,统计报表的呈现形式为“箱线图”。
详细说明
- 数据解密
医疗保险项目是一个中间件,它会提供一些接口,用来查询提供患者、医院等相关系统数据。它使用的数据库我们称之为主题库,即针对某项应用而所设计的数据集合,如本项目为医疗保险。在主题库的上层还有个库叫做标准库,标准库具有最全的数据。标准库的数据来自于原始库,而原始库中的数据来自于各大医院。在医疗保险项目的下层,还有一些中间件,他们做着类似于etl的工作,将标准库中的一些数据放到我所使用的主题库中。因需求变动,现在这些中间件会将一些敏感字段信息进行加密,然后放到我所使用的主题库中。而我在查询这些数据时,需要将这些数据解密,提供给调用我的接口的调用者。 - 箱线图
网关管理项目是基于kong网关并根据公司的实际应用场景所开发的上层应用。用于kong网关相关参数配置以及报表分析。在报表分析中,需要求出每个“微服务”在某段时间内的最快响应时间,最慢响应时间,以及这段时间内的“上四分数”、“中位数”和“下四分数”。原型设计如下:
这里面的难点在于“四分数”的求取。
解题思路
1.数据解密
已知条件
- 首先,单纯的数据查询相关功能以及完成,但这不涉及到将解密。
- 我的下层提供了一个“加密配置”接口,这个接口会告诉我那些表的那些字段被加密了。
- 加密配置会存在变动,他们会将这些变动信息放到kafka中。
解决思路
- 项目启动后,我需要将所有的配置信息加载进来,即项目在启动后就调用他们提供的“加密配置”接口,然后将这些数据放在内存中。
- 监听kafka,一旦获取到配置变动信息,就对之前放置在内存中的“加密配置”信息进行更新。
- 在dao层设置aop,根据配置对数据库中获取的数据进行解密。
具体实现
步骤一:启动时就加载配置信息可以通过两种方式来完成。第一个是使用spring-boot 的事件模型,即实现ApplicationListener<ApplicationReadyEvent>并注入到spring容器中。第二个是实现CommandLineRunner 。本项目使用的第一个方式,实现逻辑如下:
1.自定义spring-boot监听器
@Component
public class ApplicationStartListener implements ApplicationListener<ApplicationReadyEvent> {
Logger logger = LoggerFactory.getLogger(ApplicationStartListener.class);
//远程调用服务
@Autowired
RpcService rpcService;
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
rpcService.loadEncryptionConfig();
}
}
2.远程调用配置接口服务
@Service
public class RpcService {
private Logger logger = LoggerFactory.getLogger(RpcService.class);
//配置信息涉及到并发操作,故集合使用CopyOnWriteArrayList
public static Map<String,CopyOnWriteArrayList<String>> encryptMap = new HashMap<>(100);
@Autowired
RestTemplate restTemplate;
public void loadEncryptionConfig(){
try {
HttpEntity<String> httpEntity = new HttpEntity<>(null,new HttpHeaders());
ResponseEntity<String> exchange = restTemplate.exchange(ENCRYPT_URL, HttpMethod.GET, httpEntity, String.class);
//相应的业务逻辑
} catch (RestClientException e) {
logger.error("load encrypt config error");
}
}
}
3.kafka监听加密配置变动
@Component
public class MyKafkaListener {
private static final Logger log = LoggerFactory.getLogger(MyKafkaListener.class);
@KafkaListener(id = "${spring.kafka.consumer.client-id}", topics = "${spring.kafka.template.default-topic}")
public void consumerListener(ConsumerRecord<String, String> record) {
log.info("receive : " + record.toString());
//具体的业务逻辑 略
}
}
4.在dao层设置aop
这里有一个难点,就是程序如何知道从数据库中得到对象需要解密。这里我们可以使用自定义注解来告诉程序这个对于来源于某个表,如果这个表有加密配置,你应当对他的相关属性(对于与表的列)进行解密操作。
自定义注解 MyTableName
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyTableName {
String tableName() default "";
}
自定义注解 MyColumnName
//一些特殊的列可能需要改注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyColumnName {
String columnName();
}
为pojo对象使用注解
public class MyPojo extends Model<MyPojo > {
private static final long serialVersionUID = 1L;
@TableId(value = "PKID", type = IdType.UUID)
private String pkid;
private String otherColumn;
}
为dao层使用aop
@Aspect
@Component
public class DaoAspect {
private Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("execution(public * com.cdqd.app.insurance.mapper.*.*.*(..))")
public void daoPointcut() {
}
@Before("daoPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
}
@AfterReturning(returning = "ret", pointcut = "daoPointcut()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
if (ret != null) {
//返回的内容是list
if (ret instanceof List) {
List retList = (ArrayList) ret;
for (Object pojo : retList) {
handPojo(pojo);
}
}
//返回的内容是page
else if (ret instanceof IPage){
IPage retPage = (Page)ret;
for (Object pojo : retPage.getRecords()) {
handPojo(pojo);
}
}
//返回的内容是pojo
else {
handPojo(ret);
}
}
}
private void handPojo(Object ret) throws IllegalAccessException {
Class<?> aClass = ret.getClass();
//获取表注解
MyTableName myTableName = aClass.getAnnotation(MyTableName.class);
//查看表注解内容是否包含在配置中
if (myTableName == null || encryptMap.get(myTableName.tableName()) == null) {
return;
}
//包含则对属性进行处理
Field[] fields = aClass.getDeclaredFields();
for (Field field : fields) {
List<String> columnConfig = encryptMap.get(myTableName.tableName());
//获取列名
String columnName = "";
MyColumnName myColumnName = field.getAnnotation(MyColumnName.class);
//从注解中获取
if (myColumnName == null) {
columnName = field.getName();
} else {
//使用属性名作为列名
columnName = myColumnName.columnName();
}
//反射处理
field.setAccessible(true);
Object value = field.get(ret);
//只对串类型进行处理,因为加密后产生的必定是串
if (value instanceof String == false) {
continue;
}
//解密
for (String temp : columnConfig) {
if (columnName.compareToIgnoreCase(temp.replaceAll("_", "")) == 0) {
try {
String newValue = EnDecryptionUtils.sm2Decrypt(value.toString());
field.set(ret, newValue);
}catch (Exception e){
logger.error("en decrypt fail for {}",value);
}
}
}
}
}
/**
* 获取请求方法
*
* @param jp
* @return
*/
public Method getInvokedMethod(JoinPoint jp) {
// 被调用方法名称
String methodName = jp.getSignature().getName();
Method method = null;
Method[] methods = jp.getTarget().getClass().getMethods();
for (Method temp : methods) {
if (temp.getName().equals(methodName)) {
method = temp;
}
}
return method;
}
至此,这个需求就完成了。
2.查询四分位数
已知条件
- 个别微服务存在实时性接口,调用的次数一天甚至有数十万,一个微服务将产生近百万的访问记录,数据量随着时间会线性增长。
- 箱线图中每一个箱子统计结果由起止时间和截止时间节点。
解决思路
基于上面需求想到过3种方案。最开始想到的是将所有的响应时间全部查询出来,然后通过java代码进行计算,但想到如果用户选择的总时间段为30天(甚至更长),并分成30段(及箱线图显示成30个箱子),也就是说每个箱子的统计结果时间段位为1天,而一天的数据量近百万,纵使能够很快查询出来,但将查询的结果通过i/o返回到本地也会花费大量时间,用户并不能立即得到相应结果,故放弃了该方案。接着想到的就是定时任务完成,通过定时任务每30分钟计算出每个微服务的响应时间的箱线图数据,并创建一个张表将其保存下来,用户查询时从结果表中查询,但是这样存在一个很大的问题,他的最小时间粒度为30分钟,并且其他时间粒度必须为30分钟的倍数(1小时,1小时30分钟…),用户无法自定义显示箱线图箱子展示数,该值必须和前台约定写死。最后,想到一个好方法,通过多条sql分步查询出该结果,首先查出该段时间内的微服务的响应次数,然后通过嵌套查询和聚合函数limit就可以轻松得到相应的四分位数。
具体实现
1.箱线图实体
@ApiModel(value = "BoxPlot")
public class BoxPlot {
/**
* 组数
*/
@ApiModelProperty("组数")
private int boxNumber;
/**
* 组宽度 秒
*/
@ApiModelProperty("组宽度 秒")
private long boxWidth;
@ApiModelProperty("箱线图数据")
private List<Box> boxList;
public BoxPlot() {
}
public BoxPlot(int boxNumber, long boxWidth, List<Box> boxList) {
this.boxNumber = boxNumber;
this.boxWidth = boxWidth;
this.boxList = boxList;
}
public int getBoxNumber() {
return boxNumber;
}
public void setBoxNumber(int boxNumber) {
this.boxNumber = boxNumber;
}
public long getBoxWidth() {
return boxWidth;
}
public void setBoxWidth(long boxWidth) {
this.boxWidth = boxWidth;
}
public List<Box> getBoxList() {
return boxList;
}
public void setBoxList(List<Box> boxList) {
this.boxList = boxList;
}
}
2.箱线图中的每个箱子实体
@ApiModel(value = "Box")
public class Box {
/**
* 上边缘
*/
@ApiModelProperty("上边缘")
private Integer top;
/**
* 下边缘
*/
@ApiModelProperty("下边缘")
private Integer bottom;
/**
* 中位数
*/
@ApiModelProperty("中位数")
private Integer median;
/**
* 上四分位
*/
@ApiModelProperty("上四分位")
private Integer topQuartile;
/**
* 下四分位
*/
@ApiModelProperty("下四分位")
private Integer bottomQuartile;
/**
* 横坐标 起始时间
*/
@ApiModelProperty("起始时间")
private LocalDateTime begin;
/**
* 横坐标 终止时间
*/
@ApiModelProperty("终止时间")
private LocalDateTime end;
public Box() {
}
public Box(Integer top, Integer bottom, Integer median, LocalDateTime begin, LocalDateTime end) {
this.top = top;
this.bottom = bottom;
this.median = median;
this.begin = begin;
this.end = end;
}
public Integer getTop() {
return top;
}
public void setTop(Integer top) {
this.top = top;
}
public Integer getBottom() {
return bottom;
}
public void setBottom(Integer bottom) {
this.bottom = bottom;
}
public Integer getMedian() {
return median;
}
public void setMedian(Integer median) {
this.median = median;
}
public Integer getTopQuartile() {
return topQuartile;
}
public void setTopQuartile(Integer topQuartile) {
this.topQuartile = topQuartile;
}
public Integer getBottomQuartile() {
return bottomQuartile;
}
public void setBottomQuartile(Integer bottomQuartile) {
this.bottomQuartile = bottomQuartile;
}
public LocalDateTime getBegin() {
return begin;
}
public void setBegin(LocalDateTime begin) {
this.begin = begin;
}
public LocalDateTime getEnd() {
return end;
}
public void setEnd(LocalDateTime end) {
this.end = end;
}
}
3.controller
@Controller
@RequestMapping("accessLog")
@Api(value = "统计", tags = "AccessLogController",description = "应用服务统计分析接口")
public class AccessLogController {
@Autowired
IAccessLogService accessLogService;
@ApiOperation("服务响应图")
@RequestMapping(value = "service/responseAnalysis", method = RequestMethod.GET)
@ResponseBody
public MyResponseEntity<BoxPlot> responseAnalysis(
@ApiParam("服务id") @RequestParam("id") String id,
@ApiParam(value = "开始时间") @RequestParam("begin") LocalDateTime begin,
@ApiParam(value = "截止时间") @RequestParam("end") LocalDateTime end,
@ApiParam(value = "分段数 5 <= number <= 30") @RequestParam("number") int number) {
if (StringUtils.isEmpty(id)) {
return new MyResponseEntity<>(ERROR_CODE, "服务id丢失");
}
if (number < MIN_SECTION || number > MAX_SECTION) {
return new MyResponseEntity<>(ERROR_CODE, "分段数不合理");
}
if (end.isBefore(begin)) {
return new MyResponseEntity<>(ERROR_CODE, "开始时间大于截止时间");
}
BoxPlot boxPlot = accessLogService.responseAnalysis(id, begin, end, number);
return new MyResponseEntity().buildSuccess("boxPlot", boxPlot);
}
}
4.service
@Service
public class AccessLogServiceImpl extends ServiceImpl<AccessLogMapper, AccessLog> implements IAccessLogService {
@Autowired
AccessLogMapper accessLogMapper;
@Override
public BoxPlot responseAnalysis(String id, LocalDateTime begin, LocalDateTime end, int number) {
//时间总长度 second
long length = (datetimeToEpochMilli(end) - datetimeToEpochMilli(begin)) / 1000;
//每段时间长度 second
long sectionLength = length / number;
//每段时间的起点和终点
LocalDateTime beginTemp = begin;
LocalDateTime endTemp = null;
//箱型图对象
BoxPlot boxPlot = new BoxPlot(number, sectionLength, null);
List<Box> boxList = new ArrayList<>(20);
while (number > 0) {
number--;
if (number == 0) {
endTemp = end;
} else {
endTemp = beginTemp.plusSeconds(sectionLength);
}
//sql query
//查询总访问量,用于辅助四分位数的查询
Integer countForService = accessLogMapper.accessCountForService(id, beginTemp, endTemp);
//查询下边缘
Integer bottomResponse = accessLogMapper.minResponse(id, beginTemp, endTemp);
//查询上边缘
Integer topResponse = accessLogMapper.maxResponse(id, beginTemp, endTemp);
//查询上四分位
Integer topQuartileResponse = accessLogMapper.quartilResponse(id, beginTemp, endTemp, countForService / 4);
//查询中位数
Integer medianResponse = accessLogMapper.quartilResponse(id, beginTemp, endTemp, countForService / 2);
//查询下四分位
Integer bottomQuartileResponse = accessLogMapper.quartilResponse(id, beginTemp, endTemp, countForService - (countForService / 4));
Box box = new Box();
box.setBegin(beginTemp);
box.setEnd(endTemp);
box.setTop(topResponse);
box.setBottom(bottomResponse);
box.setTopQuartile(topQuartileResponse);
box.setMedian(medianResponse);
box.setBottomQuartile(bottomQuartileResponse);
boxList.add(box);
beginTemp = beginTemp.plusSeconds(sectionLength);
}
//组装
boxPlot.setBoxList(boxList);
return boxPlot;
}
}
4.四分位SQL(mybatis)
<select id="quartilResponse" resultType="java.lang.Integer">
select request_time
from (
select request_time
from access_log
where access_time > #{begin} and access_time < #{end} and service_id = #{id}
order by request_time asc
) as result
limit #{offset},1
</select>