-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
使用 @ContainerCache 添加缓存后,即使缓存全部命中依然会调用查询方法 #304
Comments
首先,参见文档中的缓存部分,如果你指的“缓存”是通过在数据源上添加 不过,如果你纠结的是为什么单次批量执行和循环单次执行查库次数不一样,那么批量查询的过程中,实际上并没有任何缓存,只查一次是因为 Crane4j 的执行器自动合并了数据源相同的查询。 如果你感觉有点抽象,那么你可以把 Crane4j 的填充行为理解成这样一个方法: public void fill(Collection<targets> targets) {
// 将待填充的对象根据key分组
Map<Integer, List<Foo>> keyWithTarget = targets.stream()
.collect(Collectors.groupBy(Foo::getId));
Set<Integer> keys = keyWithTarget.keySet();
// 根据key查询数据源
Map<Integer, Data> dataWithKey = datasource.list(keys).stream()
.collect(Collectors.toMap(Data::getId, Function::identity));
// 根据key获取待填充对象对应数据,然后进行填充
keyWithTarget.forEach((key, foos) -> {
Data data = dataWithKey.get(key);
if (Objects.nonNull(data)) {
// 执行填充
foos.forEach(foo -> foo.setName(data.getName));
}
]})
} 你每次调用 |
public class TaskExecutionDataResp{
@Assemble(container = "task", props = @Mapping(src = "name", ref = "taskName"))
private Long taskId;
private String taskName;
}
// TasksServiceImpl
@GuavaContainerCache
@ContainerMethod(
namespace = "task",
resultType = TasksDO.class, resultKey = "id"
)
public TasksDO getById(Long id) {
return baseMapper.selectById(id);
}
// query method
@Override
public PageResp<TaskExecutionDataResp> page(TaskExecutionDataQuery query, PageQuery pageQuery) {
QueryWrapper<TaskExecutionDataDO> queryWrapper = this.buildQueryWrapper(query);
IPage<TaskExecutionDataDO> page = ((BaseMapper) this.baseMapper).selectPage(pageQuery.toPage(), queryWrapper);
PageResp<TaskExecutionDataResp> pageResp = PageResp.build(page, this.listClass);
List<TaskExecutionDataResp> list = pageResp.getList();
Crane4jTemplate bean = SpringUtil.getBean(Crane4jTemplate.class);
// 触发了10次task getById的查询,正常应该只会触发一次,后续从缓存获取了吧?
list.forEach(taskExecutionDataResp -> bean.execute(Collections.singleton(taskExecutionDataResp)));
return pageResp;
} 我指的就是数据源加缓存的情况,那看起来应该是缓存没生效?还需要其他什么配置么? |
问题 你是对的,我用下面这个测试用例验证了一下,现在缓存确实存在问题: @RunWith(SpringRunner.class)
@ContextConfiguration(classes = { DefaultCrane4jSpringConfiguration.class, GitHub304Test.ServiceImpl.class })
public class GitHub304Test {
@Autowired
private Crane4jGlobalConfiguration configuration;
@Autowired
private ServiceImpl serviceImpl;
@Test
public void test() {
Container<?> container = configuration.getContainer("test");
Assert.assertTrue(container instanceof CacheableContainer);
CacheableContainer<?> cacheableContainer = (CacheableContainer<?>) container;
CacheDefinition cacheDefinition = cacheableContainer.getCacheDefinition();
Assert.assertEquals(CacheManager.DEFAULT_GUAVA_CACHE_MANAGER_NAME, cacheDefinition.getCacheManager());
Map<Integer, Foo> data1 = (Map<Integer, Foo>) ((Container<Integer>)container).get(Collections.singletonList(1));
Assert.assertEquals(1, data1.size());
Foo foo1 = data1.get(1);
Assert.assertNotNull(foo1);
Assert.assertEquals(1, serviceImpl.getCounter().get());
Map<Integer, Foo> data2 = (Map<Integer, Foo>) ((Container<Integer>)container).get(Collections.singletonList(1));
Assert.assertEquals(1, data2.size());
Foo foo2 = data2.get(1);
Assert.assertNotNull(foo2);
Assert.assertSame(foo1, foo2); // 缓存确实生效了
Assert.assertEquals(1, serviceImpl.getCounter().get()); // 断言失败,实际的 counter 为 2
}
@Component
public static class ServiceImpl {
@Getter
private AtomicInteger counter = new AtomicInteger(0);
@GuavaContainerCache
@ContainerMethod(namespace = "test", resultType = Foo.class)
public List<Foo> getByIds(Collection<Integer> ids) {
counter.incrementAndGet();
return ids.stream()
.map(Foo::new)
.collect(Collectors.toList());
}
}
@Data
@RequiredArgsConstructor
public static class Foo {
private final Integer id;
}
} 根据上面的用例,如果缓存生效,理论上 不过问题在于,第二次调用缓存确实生效了,因为前后两次获取的对象都一个对象,然而即使缓存全部命中,依然查了库,只不过第二次查的时候传入的就是一个空集合了。 这个问题在于 @SuppressWarnings("unchecked")
@Override
public Map<K, ?> get(Collection<K> keys) {
CacheObject<K> current = getCurrentCache();
Map<K, Object> caches = current.getAll(keys);
// all keys are not cached?
if (caches.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("get none cached keys [{}] from container [{}]", keys, container.getNamespace());
}
Map<K, Object> values = (Map<K, Object>)container.get(keys);
current.putAll(values);
return values;
}
// some keys are cached?
keys = keys.stream()
.filter(k -> !caches.containsKey(k)).collect(Collectors.toSet());
// FIX:这个地方如果缓存全部命中,那么 keys 应该是空的,此时应该直接返回数据,而不是继续执行下去
if (log.isDebugEnabled()) {
log.debug("get none cached keys [{}] from container [{}]", keys, container.getNamespace());
}
Map<K, Object> values = (Map<K, Object>)container.get(keys);
current.putAll(values);
// merge cached values and none cached values
caches.putAll(values);
return caches;
} 解决方案 目前应该所有的缓存机制都有这个问题,本周内我会发 2.8.2 修复它,在那之前,你可以先用下面这几种方案临时解决一下:
|
你说的是另一个问题,命中了缓存,但是还是会查DB,且ID都变成了NULL 我碰到的是另一种情况,刚好两种情况都被我碰到了: cn.crane4j.core.cache.GuavaCacheManager.DefaultCacheFactory#getCache public Cache<Object, Object> getCache(Long expireTime, TimeUnit timeUnit) {
Asserts.isNotEquals(expireTime, 0L, "Expire time must not be 0");
if (expireTime > 1) {
return CacheBuilder.newBuilder()
.expireAfterWrite(expireTime, timeUnit)
.build();
}
// if expire time less than 0, use weak keys and weak values
return CacheBuilder.newBuilder()
.weakKeys().weakValues()
.build();
} 这块有点问题,过期时间为1或者不设置过期时间都会走下面的build,weakKeys会导致cache的hash计算使用的com.google.common.base.Equivalence.Identity,其中会使用System.identityHashCode(o); 计算hash,会导致同一个值两次的hashcode不一致,所以命中不到cache |
好家伙,坑算是给你都踩出来了。 我研究了一下, 也就是说,如果某个类重写了 hashCode,那么这个类的两个实例即使 equals 返回 true,且 hashCode 相等,但是通过 这个问题最终会导致对于常用的 Key 类型 —— 比如 Integer、Long、String —— 来说,一旦值超过了常量池的范围,那么即使理论上 Key 应该可以命中缓存,但是实际上依然无法从 GuavaCache 中获取到缓存的数据。比如 这个问题在 2.8.2 会一并修复。 |
这个问题会在关联到 PR 以后关闭,在确认修复代码提交之前先开着吧。 |
测试了一下,貌似foreach 执行 Crane4jTemplate.execute(obj) , 每次都会重新查库,没有使用缓存
换成Crane4jTemplate.execute(list),就能正常使用缓存,同一个id只查了一次。
The text was updated successfully, but these errors were encountered: