变通实现微服务的per request以提高IO效率
效率
同一次业务操作过程中,往往会出现某种操作被重复执行,逻辑上来讲如果只执行一次是最理想的。这里所指的操作特指一些IO操作,比如从数据库中获取登录人的信息,也就是说如果一次请求中包含5个小逻辑,这5个小逻辑包含3次获取用户信息的操作,理想的情况是3次只有一次是从数据库中加载,其余的两次从缓存中获取。
- 多次调用,每个服务实现独自请求用户信息。
- 一次调用,多次读取。首先从Context中加载,如果失败从数据库中加载,最后将结果存入Context。
前提
限于非web环境,这里是dubbo实现的微服务。如果是web环境的话解决问题比较简单,因为我们可以充分利用Spring Framwork中提到的三个bean生命周期的特殊来解决:
- request
- session
- global
案例
将老的价格数据迁移成新的价格数据,这里大概是如下的步骤:
- 删除新老价格的关系,因为需要支持重复迁移
- 禁用之前已经存在的价格规则,规则是描述价格在某种场景下生效的逻辑
- 创建新的价格
- 创建新的规则
- 启用新规则
上面步骤的价格,规则,关系数据分别属于三个业务对象,自身都具备CRUD的服务接口,这些CRUD都需要记录操作人信息,记录的标准就是接口传入的操作人所持有的token,我们需要将这个token转换成userId,userName之类的信息与价格,规则等信息一并存储。
时序图如下:
问题:迁移一条价格多次读取用户信息效率低
由于迁移价格会涉及到多个对象的操作,而操作这些具体业务对象的接口并不支持传具体的userId,userName只支持token,所以不可避免的会在保存价格等信息时各自去根据token查询操作人信息。实测一个价格完成一次数据迁移涉及到获取用户信息的次数多达20+次,效率是比较低,如何去解决呢?
现状
由于我目前实现的微服务是无状态的,也不是web环境,所以上面提到的那些bean的作用域功能就使用不上。
目标
实现类似request作用域的功能,一次请求仅执行一次,其余的请求从缓存中获取结果以提高IO操作效率。
方案
可采用TreadLocal来当缓存,存储频繁读取的数据。增加了CacheContext,获取用户首先从CacheContext中取,如果为空则从数据库加载然后回写到Treadlocal中,下一次再请求用户信息时就可以命中缓存不需要再次从数据库中加载,显然效率得到了质的提升。
时序图如下:
实现步骤如下:
- 创建Context数据对象
缓存的容器为TreadLocal,可以定义多个需要缓存的属性,并提供一个清除所有缓存的方法。清除的方法是必做的,否则会一直停留在线程中,当线程被再次利用时会获取到上一次请求存储的数据。
@Service
public class ProductContext {
private static Logger logger = Logger.getLogger(CiaServiceImpl.class);
private ThreadLocal<CiaUserInfo> ciaUserInfoThreadLocal=new ThreadLocal<>();
private void clearCiaUserInfo(){
this.ciaUserInfoThreadLocal.remove();
this.logger.info("清除getTokenInfo缓存成功");
}
public void setCiaUserInfoToCache(CiaUserInfo ciaUserInfo){
this.clearCiaUserInfo();
this.ciaUserInfoThreadLocal.set(ciaUserInfo);
this.logger.info("将getTokenInfo存储到缓存中");
}
public CiaUserInfo getCiaUserInfoFromCache(String token){
CiaUserInfo ciaUserInfo =this.ciaUserInfoThreadLocal.get();
if(null!=ciaUserInfo){
this.logger.info("从缓存中获取到用户信息getTokenInfo");
}
return ciaUserInfo;
}
public void clearAll(){
this.clearCiaUserInfo();
this.logger.info("清除ProductContext的缓存成功");
}
}
- 修改获取用户信息的服务类,结合Context操作缓存。
public CiaUserInfo getTokenInfo(String token) throws Exception {
CiaUserInfo result = this.productContext.getCiaUserInfoFromCache(token);
if(null!=result){
return result;
}
else {
result=new CiaUserInfo();
}
//...get user from db
this.productContext.setCiaUserInfoToCache(result);
return result;
}
- 增加注解,用来标识哪些方法是需要使用缓存的
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LocalCacheContext {
/**
* 是否启动
* @return
*/
boolean enable() default true;
}
- 增加拦截器
TreadLocal中存入的信息需要有效及时的释放,配合上面申明的注解来完成。
@Aspect
public class LocalCacheContextInterceptor {
private Logger logger = LoggerFactory.getLogger(getClass().getName());
@Autowired
private ProductContext productContext;
@Pointcut("execution(* product.service.service.impl.*.*(..))")
public void pointCut() {
}
@After("pointCut()")
public void after(JoinPoint joinPoint) throws ProductServiceException {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
LocalCacheContext localCacheContext= targetMethod.getAnnotation(LocalCacheContext.class);
if(null!=localCacheContext){
this.productContext.clearAll();
}
}
}
- 客户端调用
@LocalCacheContext
public void migrationPrice(Long priceId) throws ProductServiceException {
this.migrationPriceService.migrationPrice(priceId);
}