概述
近期朋友出去面试,总结了几个常见高频问题,咱们来一起看下这些问题,咱们从以下三点来分析
-
面试官的心里是怎样的
-
如何在不会的情况下绕过主题回答
-
从源码角度分析回答
1 有用过@ Cacheable吗,说下底层的原理。
面试管心里 :个人猜想,主要是想考察面试人员spring的缓存框架原理和常规的集中对外缓存。
如果没用过这个问题如何回答:大概分为两种情况,一种了解点@Cacheable实际没用过,一种压根就听说过,见名知意,这是个缓存相关的,那就围绕着缓存来解答。
例:这个注解没用过,我们实际项目中用过分部署缓存redis memcache等,我们在项目中是这么使用的。。。。。。。
spring 这个缓存框架有点类似于mybatis的二级缓存,个人感觉没啥太大用途,实际生产环境中我们都是集群部署,相同条件的多次请求很小程度打到同一个实例上,不如用分部署缓存命中率高,如果一个实例上过多的这种缓存会导致jvm的gc加重,当然可以和redis整合,但是如果内部方法调用的时候就失效了,因为它是基于aop实现的。
例:这个注解没用过,你说的这个他是spring的还是orm的?还是一种pass产品,从面试官中套出这个东西是谁的进而推断出大概作用。
在脑中快速搜索你所了解的技术中与之相同作用的说给面试官。或者根据名称可是缓存相关的,就围绕你所了解的缓存知识说。
从源码角度解说该注解原理:其实底层还是通过代理的机制,我们操作业务层某个方法的时候并不是直接操作的这个service对象,而是代理对象。
有这么几个核心类 :
CacheInterceptor
CacheProxyFactoryBean
CacheAspectSupport
CacheOperation
我们执行方法以后会进入CacheInterceptor的invoke方法中源码如下:
@Override
@Nullable
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
return invocation.proceed();
}
catch (Throwable ex) {
throw new CacheOperationInvoker.ThrowableWrapper(ex);
}
};
Object target = invocation.getThis();
Assert.state(target != null, "Target must not be null");
try {
//调用父类CacheAspectSupport的exceute方法
return execute(aopAllianceInvoker, target, method, invocation.getArguments());
}
catch (CacheOperationInvoker.ThrowableWrapper th) {
throw th.getOriginal();
}
}
进而调用其父类CacheAspectSupport:
@Nullable
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
if (this.initialized) {
Class<?> targetClass = getTargetClass(target);
CacheOperationSource cacheOperationSource = getCacheOperationSource();
if (cacheOperationSource != null) {
Collection operations = cacheOperationSource.getCacheOperations(method, targetClass);
if (!CollectionUtils.isEmpty(operations)) {
return execute(invoker, method,
new CacheOperationContexts(operations, method, args, target, targetClass));
}
}
}
return invoker.invoke();
}
getCacheOperations获得cacheOperations后调用的execute是关键,cacheOperations也就是AnnotationCacheOperationSource,它负责三个注解的调用:@Cacheable、@CachePut和@CacheEvict。
然后执行最最核心方法:
@Nullable
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
// Special handling of synchronized invocation
if (contexts.isSynchronized()) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
try {
return wrapCacheValue(method, handleSynchronizedGet(invoker, key, cache));
}
catch (Cache.ValueRetrievalException ex) {
// Directly propagate ThrowableWrapper from the invoker,
// or potentially also an IllegalArgumentException etc.
ReflectionUtils.rethrowRuntimeException(ex.getCause());
}
}
else {
// No caching required, only call the underlying method
return invokeOperation(invoker);
}
}
// Process any early evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// Check if we have a cached item matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// 如果当前没有缓存,就讲数据加载到缓存中
List cachePutRequests = new ArrayList<>();
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
if (cacheHit != null && !hasCachePut(contexts)) {
// If there are no put requests, just use the cache hit
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// Invoke the method if we don't have a cache hit
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
源码上都有相关注释,大家可以回去看下这几个关键性地方,然后debug看下调用栈,思路就更加清晰了。
2 说下BeanPostProcsessor,以及你们项目项目中使用场景。
面试官心里:考察面试者对spring 源码理解程度,有没有扩展过spring,有没有了解过其他开源产品是如何整合spring的。
如果没用过这个问题如何回答:说实在的这个要是不知道,还真不好绕过去,这个确实不清楚的话只能实话实说了。
我们实际开发中处处和这个东西打交道的,比如我们常用的Sentinel就是通过后置处理器干扰bean实例化过程来增强RestTemplete。
从源码角度解说下beanPostProccessor:关于bean potProcessor我之前专门写过一篇朋友们看下这个《2019年最后一天,呕心沥血之作,解读spring5大后置处理器》。
还有mybatis也是基于spring的扩展机制,只是它是通过BeanFactoryPostProcsessor进行的扩展。
3 一个接口多个实现类,如何将实现类交给spring(注解不含@Component)
面试官心里:有没有真实的spring开发经验,对spring常用bean实例化方式是否熟悉。
这个是spring的基础,可以通过@Bean注解来实例化对象交由spring管理,通过一接口的不同实现类可以通过bean中的name区分不同的bean名称,注入的时候采用@Resource。
源码角度解说@Bean: spring源码这块有点长,复杂,咱们简化说明重点方法。
咱们重点看下这个类
org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass方法
这个方法中对ComponentScans.class 、importResource.class、PropertySources.classComponent.class、ImportSelector.class、 Bean.class 都做了处理。
这块就是处理bean注解的地方,将带有bean注解的方法封装成BeanMethod对象,并添加到一个set集合中。
private Set retrieveBeanMethodMetadata(SourceClass sourceClass) {
AnnotationMetadata original = sourceClass.getMetadata();
Set beanMethods = original.getAnnotatedMethods(Bean.class.getName());
if (beanMethods.size() > 1 && original instanceof StandardAnnotationMetadata) {
// Try reading the class file via ASM for deterministic declaration order...
// Unfortunately, the JVM's standard reflection returns methods in arbitrary
// order, even between different runs of the same application on the same JVM.
try {
AnnotationMetadata asm =
this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata();
Set asmMethods = asm.getAnnotatedMethods(Bean.class.getName());
if (asmMethods.size() >= beanMethods.size()) {
Set selectedMethods = new LinkedHashSet<>(asmMethods.size());
for (MethodMetadata asmMethod : asmMethods) {
for (MethodMetadata beanMethod : beanMethods) {
if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) {
selectedMethods.add(beanMethod);
break;
}
}
}
if (selectedMethods.size() == beanMethods.size()) {
// All reflection-detected methods found in ASM method set -> proceed
beanMethods = selectedMethods;
}
}
}
catch (IOException ex) {
logger.debug("Failed to read class file via ASM for determining @Bean method order", ex);
// No worries, let's continue with the reflection metadata we started with...
}
}
return beanMethods;
}
然后循环beanMethod集合,将beanMethod转化为Beandefinition
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
//循环加载bean
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
//省略部分代码
String initMethodName = bean.getString("initMethod");
if (StringUtils.hasText(initMethodName)) {
beanDef.setInitMethodName(initMethodName);
}
String destroyMethodName = bean.getString("destroyMethod");
beanDef.setDestroyMethodName(destroyMethodName);
//....省略部分代码
this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}
4 用户订单分库分表如何去做
面试官心里:对分库分表是否了解,是否了解分库分表后带来的跨分片查询的问题等等
没有实际如何回答:这块如果确实没有实际做过,并且平时没有关注这块的问题,那这个最好如实说就完了,因为如何分片不合理会造成分布式事务以及跨分片查询等问题。
个人认为较为合适的方案如下:
以用户id为分片键,采用hash取模方式,这样相同用户的订单会在同一个分片上,后续如果用户需要查询历史订单修改订单之类的,只需操作这个一分片即可。也可以避免分布式事务问题。
如果采用其他字段作为分片键,这样同一个用户的订单数据极有可能分布在多个分片上,如果用户查询历史订单需要执行广播sql,在所有分片上查询一遍然后聚合返回,这样性能会大大降低,并且如果用户同同时删除几个历史订单或者同时提交多个订单分布在不同的分片会引入分布式事务问题。
分布式事务我们是能避免的话尽量避免,毕竟解决这个问题需要的代价太大了。
可能面试官还会接着问扩容的问题:
如果采用hash取模的方式后续扩容是需要数据迁移的,需要重新将数据按照最新的分片规则进行划分,但是这种算法性能较高,数据分布较均匀。
如果采用枚举分片,按照不同地域的用户进行分片,扩容时无需数据迁移,但是数据会有倾斜即较大省份的库数据会明显居多。
我个人偏向于hash取模的方式,我们如果前期规划好的话几年之内是没问题的,即便业务突增或者几年后需要扩容,可以逐个idc进行扩容。现在大点的公司几乎都是多中心的。
开源的数据库中间件有好多,但是目前看比较稳定,社区比较活跃的当属shardingsphere 大家可以去了解下,后续我们也会对此做源码分析专题。
总结
今天先说这4个面试题,明天咱们继续,由于个人能力有限,如果存在争议之处还望私信指出