fix(cache): 解决缓存中JSON null值导致的空指针问题

- 添加对历史缓存中JSON "null" 字符串的兼容处理
- 当缓存解析出null值时清理缓存并回源数据库
- 在CmsWebsiteServiceImpl中增加缓存清理逻辑
- 在ShopWebsiteServiceImpl中统一缓存异常处理机制
- 添加单元测试验证JSON null值场景的正确回退行为
This commit is contained in:
2026-02-06 00:49:00 +08:00
parent 2c076e2b0f
commit 60279fca4c
3 changed files with 78 additions and 2 deletions

View File

@@ -337,9 +337,16 @@ public class CmsWebsiteServiceImpl extends ServiceImpl<CmsWebsiteMapper, CmsWebs
if (StrUtil.isNotBlank(siteInfo)) {
log.info("从缓存获取网站信息租户ID: {}", tenantId);
try {
return JSONUtil.parseObject(siteInfo, ShopVo.class);
// 兼容历史缓存JSON "null" 会被解析为 null此时应视为未命中并回源数据库。
ShopVo cacheVo = JSONUtil.parseObject(siteInfo, ShopVo.class);
if (cacheVo != null) {
return cacheVo;
}
log.warn("网站信息缓存命中但内容为空(null)清理缓存后回源数据库租户ID: {}", tenantId);
redisUtil.delete(cacheKey);
} catch (Exception e) {
log.warn("缓存解析失败,从数据库重新获取: {}", e.getMessage());
log.warn("缓存解析失败,清理缓存后从数据库重新获取: {}", e.getMessage());
redisUtil.delete(cacheKey);
}
}

View File

@@ -51,6 +51,9 @@ public class ShopWebsiteServiceImpl implements ShopWebsiteService {
log.info("从缓存获取商城信息租户ID: {}", tenantId);
return cacheVo;
}
// 兼容历史缓存JSON "null" 会被解析为 null此时清理缓存并回源。
log.warn("商城信息缓存命中但内容为空(null)清理缓存后回源数据库租户ID: {}", tenantId);
redisUtil.delete(cacheKey);
} catch (Exception e) {
log.warn("商城缓存解析失败,清理缓存后重新获取: {}", e.getMessage());
redisUtil.delete(cacheKey);

View File

@@ -0,0 +1,66 @@
package com.gxwebsoft.cms.service.impl;
import com.gxwebsoft.cms.entity.CmsWebsite;
import com.gxwebsoft.cms.mapper.CmsWebsiteMapper;
import com.gxwebsoft.cms.service.CmsNavigationService;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.shop.vo.ShopVo;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class CmsWebsiteServiceImplCacheTest {
@Test
void getSiteInfo_cacheValueIsJsonNull_shouldFallbackToDb() throws Exception {
Integer tenantId = 1;
RedisUtil redisUtil = mock(RedisUtil.class);
CmsNavigationService cmsNavigationService = mock(CmsNavigationService.class);
CmsWebsiteMapper cmsWebsiteMapper = mock(CmsWebsiteMapper.class);
// 历史缓存可能存在字符串 "null"Jackson 解析后会得到 null需回源 DB。
when(redisUtil.get("SiteInfo:" + tenantId)).thenReturn("null");
when(cmsNavigationService.listRel(any())).thenReturn(Collections.emptyList());
CmsWebsite website = new CmsWebsite();
website.setTenantId(tenantId);
website.setTenantName("tenant");
website.setWebsiteName("site");
when(cmsWebsiteMapper.getByTenantId(tenantId)).thenReturn(website);
CmsWebsiteServiceImpl service = new CmsWebsiteServiceImpl();
setField(service, "redisUtil", redisUtil);
setField(service, "cmsNavigationService", cmsNavigationService);
setField(service, "baseMapper", cmsWebsiteMapper);
ShopVo vo = service.getSiteInfo(tenantId);
assertNotNull(vo);
verify(redisUtil).delete("SiteInfo:" + tenantId);
verify(cmsWebsiteMapper).getByTenantId(tenantId);
}
private static void setField(Object target, String fieldName, Object value) throws Exception {
Class<?> c = target.getClass();
while (c != null) {
try {
Field f = c.getDeclaredField(fieldName);
f.setAccessible(true);
f.set(target, value);
return;
} catch (NoSuchFieldException ignored) {
c = c.getSuperclass();
}
}
throw new NoSuchFieldException(fieldName);
}
}