Skip to content

Instantly share code, notes, and snippets.

@pengxiaochao
Created June 5, 2025 06:55
Show Gist options
  • Select an option

  • Save pengxiaochao/5b09f3d978e4ba950687fab693c048a3 to your computer and use it in GitHub Desktop.

Select an option

Save pengxiaochao/5b09f3d978e4ba950687fab693c048a3 to your computer and use it in GitHub Desktop.
Mybatis 对象自动缓存插件
package com.au92.grpcapi.infrastructure.interceptor;
import com.au92.common.util.exception.ShortClassNameUtil;
import com.au92.common.util.json.JsonUtils;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.regex.Pattern;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.data.redis.core.RedisTemplate;
/**
* Redis缓存拦截器
*
* <p>基于MyBatis拦截器机制实现的二级缓存,使用Redis作为缓存存储。
* 支持以下功能:</p>
* <ul>
* <li>单条记录的主键查询缓存</li>
* <li>批量查询的mget优化(兼容Redis Cluster)</li>
* <li>更新操作的缓存失效</li>
* <li>空值缓存防穿透</li>
* <li>双删策略保证数据一致性</li>
* </ul>
*
* <p>支持的查询类型:</p>
* <ul>
* <li>简单主键等值查询:baseMapper.selectById(id)</li>
* <li>LambdaQueryWrapper单主键查询:lambdaQuery().eq(Entity::getId, id)</li>
* <li>批量主键查询:baseMapper.selectBatchIds(ids)</li>
* </ul>
*
* <p>缓存策略:</p>
* <ul>
* <li>缓存键格式:{完整类名}:主键值(使用Hash Tag确保Cluster兼容性,避免同名类冲突)</li>
* <li>正常数据缓存时间:1天(带随机浮动±2.4小时,防止缓存雪崩)</li>
* <li>空值缓存时间:1分钟(带随机浮动±6秒,防止缓存穿透)</li>
* <li>浮动过期时间:在基础过期时间上增加±10%的随机时间,避免大量缓存同时过期</li>
* </ul>
*
* <p>Redis Cluster兼容性:</p>
* <ul>
* <li>使用Hash Tag语法确保同一实体类的key在同一槽位</li>
* <li>批量操作优先使用multiGet,失败时自动回退到逐个获取</li>
* <li>完全兼容单机Redis和Redis Cluster部署</li>
* </ul>
* <li>更新时采用双删策略:操作前删除 -> 执行数据库操作 -> 操作后再删除</li>
* </ul>
*
* @author p_x_c
* @version 1.0
* @since 2024-01-01
*/
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Slf4j
public class RedisCacheInterceptor implements Interceptor {
/**
* 默认缓存过期时间(秒) - 1天
*/
private static final long DEFAULT_CACHE_EXPIRE_TIME = 86400;
/**
* 空值缓存时间(秒) - 1分钟,防止缓存穿透
*/
private static final long NULL_CACHE_EXPIRE_TIME = 60;
/**
* 空值标识符,用于标识数据库中不存在的记录
*/
private static final String NULL_VALUE_PLACEHOLDER = "NULL_VALUE";
/**
* 随机数生成器,用于生成浮动过期时间,防止缓存雪崩
*/
private static final Random RANDOM = ThreadLocalRandom.current();
/**
* 性能优化:缓存Field查找结果,避免重复反射
*/
private static final ConcurrentHashMap<Class<?>, Field> ID_FIELD_CACHE = new ConcurrentHashMap<>();
/**
* 性能优化:缓存实体类推断结果,避免重复类加载和反射
*/
private static final ConcurrentHashMap<String, Class<?>> ENTITY_CLASS_CACHE = new ConcurrentHashMap<>();
/**
* 性能优化:缓存Field访问器,避免重复设置setAccessible
*/
private static final ConcurrentHashMap<Field, Function<Object, Object>> FIELD_ACCESSOR_CACHE = new ConcurrentHashMap<>();
/**
* 将驼峰命名转换为下划线命名 性能优化:避免重复编译正则表达式
*/
private static final Pattern CAMEL_TO_UNDERSCORE_PATTERN = Pattern.compile("([a-z])([A-Z])");
@Resource
private RedisTemplate<String, Object> redisTemplate;
public RedisCacheInterceptor() {
}
/**
* MyBatis拦截器核心方法
*
* @param invocation 方法调用信息,包含目标方法、参数等
* @return 方法执行结果
* @throws Throwable 执行过程中的异常
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
String methodName = invocation.getMethod()
.getName();
if (log.isDebugEnabled()) {
log.debug("Intercepted method: {}, SQL ID: {}", methodName, ms.getId());
}
// 根据方法类型分发处理
if ("query".equals(methodName)) {
return handleQuery(invocation, ms, parameter);
} else if ("update".equals(methodName)) {
// 处理新增、更新、删除操作
return handleUpdate(invocation, ms, parameter);
}
// 其他操作直接执行,不进行缓存处理
return invocation.proceed();
}
/**
* 插件包装方法
*
* @param target 目标对象
* @return 代理对象
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 设置拦截器属性(当前实现为空)
*
* @param properties 属性配置
*/
@Override
public void setProperties(Properties properties) {
// 当前无需特殊配置
}
/**
* 构建可能的实体类路径
*/
private String[] buildPossibleEntityPaths(String packagePrefix, String entityName) {
return new String[]{packagePrefix + ".domain.model." + entityName,
packagePrefix + ".entity." + entityName,
packagePrefix + ".model." + entityName,
packagePrefix + ".domain." + entityName,
packagePrefix + ".pojo." + entityName};
}
/**
* 缓存数据库查询结果
*
* @param dbResultList 数据库查询结果列表
* @param modelClass 实体类Class对象
* @param finalResults 最终所有数据结果列表
* @param resultMap 结果映射表,存储ID到实体对象的映射关系
*/
private void cacheDatabaseResults(List<?> dbResultList, Class<?> modelClass, List<Object> finalResults, Map<Object, Object> resultMap) {
for (Object dbResult : dbResultList) {
if (dbResult != null) {
Object id = getIdValueFromEntity(modelClass, dbResult);
if (id != null) {
cacheQueryResult(getRedisKey(modelClass, id), dbResult);
finalResults.add(dbResult);
resultMap.put(id, dbResult);
}
}
}
}
/**
* 缓存空值结果(防止缓存穿透)
*
* @param missedIds 批量查询中未命中的ID列表
* @param resultMap 缓存结果id->对象映射表
* @param modelClass 实体类Class对象
*/
private void cacheNullResults(List<Object> missedIds, Map<Object, Object> resultMap, Class<?> modelClass) {
for (Object missedId : missedIds) {
if (!resultMap.containsKey(missedId)) {
cacheNullValue(getRedisKey(modelClass, missedId));
}
}
}
/**
* 缓存空值
*/
private void cacheNullValue(String redisKey) {
long expireTime = getFloatingExpireTime(NULL_CACHE_EXPIRE_TIME);
redisTemplate.opsForValue()
.set(redisKey, NULL_VALUE_PLACEHOLDER, expireTime, TimeUnit.SECONDS);
if (log.isDebugEnabled()) {
log.debug("Cached null value for key: {} with expire time: {}s", redisKey, expireTime);
}
}
/**
* 缓存查询结果
*/
private void cacheQueryResult(String redisKey, Object result) {
long expireTime = getFloatingExpireTime(DEFAULT_CACHE_EXPIRE_TIME);
redisTemplate.opsForValue()
.set(redisKey, result, expireTime, TimeUnit.SECONDS);
if (log.isDebugEnabled()) {
log.debug("Cached result for key: {} with expire time: {}s", redisKey, expireTime);
}
}
private String camelToUnderscore(String camelCase) {
return CAMEL_TO_UNDERSCORE_PATTERN.matcher(camelCase)
.replaceAll("$1_$2")
.toLowerCase();
}
/**
* 检查指定名称的实体参数
*/
private Object checkEntityParam(Class<?> modelClass, Map<String, Object> paramMap, String paramName) {
if (paramMap.containsKey(paramName)) {
Object entity = paramMap.get(paramName);
if (modelClass.isInstance(entity)) {
Object idValue = getIdValueFromEntity(modelClass, entity);
if (idValue != null) {
if (log.isDebugEnabled()) {
log.debug("Extracted ID value from '{}' parameter: {}", paramName, idValue);
}
return idValue;
}
}
}
return null;
}
/**
* 从Mapper类名中提取实体名称
*/
private String extractEntityName(String mapperClassName) {
if (mapperClassName.endsWith("Mapper")) {
// 去掉"Mapper"后缀
return mapperClassName.substring(mapperClassName.lastIndexOf('.') + 1, mapperClassName.length() - 6);
} else {
// 如果不是以Mapper结尾,取最后一个点后的部分
return mapperClassName.substring(mapperClassName.lastIndexOf('.') + 1);
}
}
/**
* 从直接的主键字段中提取ID值
*/
private Object extractIdFromDirectFields(Class<?> modelClass, Map<String, Object> paramMap) {
Field idField = getIdFieldCached(modelClass);
if (idField == null) {
return null;
}
String idFieldName = idField.getName();
if (paramMap.containsKey(idFieldName)) {
return paramMap.get(idFieldName);
}
// 也尝试检查下划线形式的字段名
String underscoreFieldName = camelToUnderscore(idFieldName);
if (paramMap.containsKey(underscoreFieldName)) {
return paramMap.get(underscoreFieldName);
}
return null;
}
/**
* 从实体对象参数中提取ID值
*/
private Object extractIdFromEntityParams(Class<?> modelClass, Map<String, Object> paramMap) {
// 检查"et"参数(entity的缩写)
Object idFromEt = checkEntityParam(modelClass, paramMap, "et");
if (idFromEt != null) {
return idFromEt;
}
// 检查"param1"参数(MyBatis的默认参数名)
Object idFromParam1 = checkEntityParam(modelClass, paramMap, "param1");
if (idFromParam1 != null) {
return idFromParam1;
}
// 遍历所有参数,查找实体对象类型的参数
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
Object value = entry.getValue();
if (modelClass.isInstance(value)) {
Object idValue = getIdValueFromEntity(modelClass, value);
if (idValue != null) {
if (log.isDebugEnabled()) {
log.debug("Extracted ID value from parameter '{}': {}", entry.getKey(), idValue);
}
return idValue;
}
}
}
return null;
}
/**
* 从Map参数中提取主键ID值
* <pre>
* 支持的Map参数键:
* - "ew": LambdaQueryWrapper等查询条件包装器
* - "param2": LambdaUpdateWrapper等更新条件包装器
* - "et": 实体对象(entity的缩写)
* - "param1": MyBatis的默认第一个参数
* - 直接的主键字段名
* </pre>
*/
private Object extractIdFromMapParameter(Class<?> modelClass, Map<String, Object> paramMap) {
if (log.isDebugEnabled()) {
log.debug("Parameter is Map with keys: {}", paramMap.keySet());
}
// 尝试从Wrapper中提取ID
Object idFromWrapper = extractIdFromWrapperParams(modelClass, paramMap);
if (idFromWrapper != null) {
return idFromWrapper;
}
// 尝试从实体对象参数中提取ID
Object idFromEntity = extractIdFromEntityParams(modelClass, paramMap);
if (idFromEntity != null) {
return idFromEntity;
}
// 尝试从直接的主键字段中提取ID
return extractIdFromDirectFields(modelClass, paramMap);
}
/**
* 从MyBatis-Plus的AbstractWrapper中提取主键ID值
* <p>
* 支持简单的主键等值查询缓存
*
* @param wrapper MyBatis-Plus查询条件包装器
* @param modelClass 实体类Class对象
* @return 提取到的主键值,如果不是简单查询则返回null
*/
private Object extractIdFromWrapper(AbstractWrapper<?, ?, ?> wrapper, Class<?> modelClass) {
try {
String sqlSegment = wrapper.getSqlSegment();
if (sqlSegment == null || sqlSegment.trim()
.isEmpty()) {
return null;
}
if (log.isDebugEnabled()) {
log.debug("Extracting ID from wrapper, SQL segment: {}", sqlSegment);
}
// 验证是否为简单的主键查询
if (!isSimpleIdWrapperQuery(sqlSegment, modelClass)) {
return null;
}
// 从Wrapper参数中提取ID值
return extractIdValueFromParams(wrapper, sqlSegment);
} catch (Exception e) {
log.warn("Failed to extract ID from wrapper: {}", e.getMessage(), e);
return null;
}
}
/**
* 从Wrapper参数中提取ID值
*/
private Object extractIdFromWrapperParams(Class<?> modelClass, Map<String, Object> paramMap) {
// 检查ew参数(MyBatis-Plus的Wrapper参数名)
if (paramMap.containsKey("ew")) {
Object wrapper = paramMap.get("ew");
if (wrapper instanceof AbstractWrapper) {
Object idValue = extractIdFromWrapper((AbstractWrapper<?, ?, ?>) wrapper, modelClass);
if (idValue != null) {
return idValue;
}
}
}
// 检查param2参数(MyBatis-Plus的LambdaUpdateWrapper等操作)
if (paramMap.containsKey("param2")) {
Object wrapper = paramMap.get("param2");
if (wrapper instanceof AbstractWrapper) {
Object idValue = extractIdFromWrapper((AbstractWrapper<?, ?, ?>) wrapper, modelClass);
if (idValue != null) {
if (log.isDebugEnabled()) {
log.debug("Extracted ID value from 'param2' wrapper: {}", idValue);
}
return idValue;
}
}
}
return null;
}
/**
* 从Wrapper参数中提取ID值
*
* @param wrapper MyBatis-Plus查询条件包装器
* @param sqlSegment 原始SQL片段
* @return 提取到的ID值
*/
private Object extractIdValueFromParams(AbstractWrapper<?, ?, ?> wrapper, String sqlSegment) {
Map<String, Object> paramNameValuePairs = wrapper.getParamNameValuePairs();
if (paramNameValuePairs == null || paramNameValuePairs.isEmpty()) {
return null;
}
if (log.isDebugEnabled()) {
log.debug("Wrapper paramNameValuePairs: {}", paramNameValuePairs);
}
// 从SQL片段中解析参数名
if (sqlSegment.contains("#{")) {
String paramName = extractParamNameFromSql(sqlSegment);
if (paramName != null) {
Object idValue = paramNameValuePairs.get(paramName);
if (idValue != null) {
if (log.isDebugEnabled()) {
log.debug("Found ID value from wrapper parameter '{}': {}", paramName, idValue);
}
return idValue;
}
}
}
// 兜底策略:如果只有一个参数,直接返回(适用于简单查询)
if (paramNameValuePairs.size() == 1) {
Object idValue = paramNameValuePairs.values()
.iterator()
.next();
if (log.isDebugEnabled()) {
log.debug("Found simple ID query for cache, ID value: {}", idValue);
}
return idValue;
}
return null;
}
/**
* 从Mapper类名中提取包路径前缀
*/
private String extractPackagePrefix(String mapperClassName) {
String packagePrefix = mapperClassName.substring(0, mapperClassName.lastIndexOf('.'));
// 如果包名以mapper或dao结尾,去掉这一层
if (packagePrefix.endsWith(".mapper") || packagePrefix.endsWith(".dao")) {
packagePrefix = packagePrefix.substring(0, packagePrefix.lastIndexOf('.'));
}
return packagePrefix;
}
/**
* 从SQL片段中提取参数名 例如从 "id = #{ew.paramNameValuePairs.MPGENVAL2}" 中提取 "MPGENVAL2"
*
* @param sqlSegment SQL片段
* @return 参数名
*/
private String extractParamNameFromSql(String sqlSegment) {
int startIndex = sqlSegment.indexOf("#{");
int endIndex = sqlSegment.indexOf("}", startIndex);
if (startIndex != -1 && endIndex != -1) {
String paramPath = sqlSegment.substring(startIndex + 2, endIndex);
if (log.isDebugEnabled()) {
log.debug("Found parameter path: {}", paramPath);
}
// 获取最后一个点号后面的部分作为参数名
int lastDotIndex = paramPath.lastIndexOf('.');
if (lastDotIndex != -1) {
return paramPath.substring(lastDotIndex + 1);
}
}
return null;
}
/**
* 查找有效的实体类
*/
private Class<?> findValidEntityClass(String[] possiblePaths, String mapperClassName) {
for (String path : possiblePaths) {
try {
Class<?> entityClass = Class.forName(path);
// 验证是否有@TableId注解的字段
if (hasTableIdField(entityClass)) {
if (log.isDebugEnabled()) {
log.debug("Found entity class from naming convention: {}", entityClass.getName());
}
return entityClass;
}
} catch (ClassNotFoundException e) {
// 继续尝试下一个路径
}
}
if (log.isDebugEnabled()) {
log.debug("Could not find entity class for mapper: {}", mapperClassName);
}
return null;
}
/**
* 获取批量缓存结果
*
* @param ids 批量ID集合
* @param modelClass 实体类Class对象
* @return 批量缓存结果,包含缓存命中和未命中的ID
*/
private BatchCacheResult getBatchCacheResults(Collection<?> ids, Class<?> modelClass) {
// 构建所有可能的Redis keys
List<String> redisKeys = new ArrayList<>();
Map<String, Object> keyToIdMap = new HashMap<>();
for (Object id : ids) {
if (id != null) {
String redisKey = getRedisKey(modelClass, id);
redisKeys.add(redisKey);
keyToIdMap.put(redisKey, id);
}
}
if (redisKeys.isEmpty()) {
return new BatchCacheResult(new ArrayList<>(), new ArrayList<>());
}
// 使用Redis Cluster兼容的批量获取缓存
List<Object> cachedResults = multiGetClusterCompatible(redisKeys);
return processBatchCacheResults(redisKeys, cachedResults, keyToIdMap);
}
/**
* 从缓存获取结果
*/
private Object getCachedResult(String redisKey) {
Object cached = redisTemplate.opsForValue()
.get(redisKey);
if (cached != null) {
if (NULL_VALUE_PLACEHOLDER.equals(cached)) {
if (log.isDebugEnabled()) {
log.debug("Cache hit for null value, key: {}", redisKey);
}
// 对于NULL_VALUE,返回空集合让MyBatis正确处理
return new ArrayList<>();
}
if (log.isDebugEnabled()) {
log.debug("Cache hit for key: {}", redisKey);
}
return cached;
}
return null;
}
/**
* 获取实体类字段名对应的数据库列名 优先使用@TableId注解的value值,否则转换为下划线命名
*
* @param field 实体类字段
* @return 数据库列名
*/
private String getColumnName(Field field) {
TableId tableId = field.getAnnotation(TableId.class);
if (tableId != null && !tableId.value()
.isEmpty()) {
return tableId.value();
}
// 默认使用字段名的下划线形式
return camelToUnderscore(field.getName());
}
/**
* 通过泛型参数获取实体类 支持 BaseMapper<Entity>, MPJBaseMapper<Entity> 等
*
* @param mapperClass Mapper类
* @return 实体类,未找到则返回null
*/
private Class<?> getEntityClassFromGeneric(Class<?> mapperClass) {
try {
Type[] genericInterfaces = mapperClass.getGenericInterfaces();
for (Type genericInterface : genericInterfaces) {
if (genericInterface instanceof ParameterizedType parameterizedType) {
Type rawType = parameterizedType.getRawType();
// 检查是否是MyBatis-Plus的Mapper接口
if (rawType instanceof Class<?> rawClass) {
String rawClassName = rawClass.getName();
// 支持常见的Mapper父接口
if (rawClassName.contains("BaseMapper") || rawClassName.contains("MPJBaseMapper") || rawClassName.endsWith("Mapper")) {
Type[] typeArguments = parameterizedType.getActualTypeArguments();
if (typeArguments.length > 0 && typeArguments[0] instanceof Class<?> entityClass) {
// 验证是否有@TableId注解的字段
if (hasTableIdField(entityClass)) {
if (log.isDebugEnabled()) {
log.debug("Found entity class from generic: {}", entityClass.getName());
}
return entityClass;
}
}
}
}
}
}
} catch (Exception e) {
log.warn("Failed to get entity class from generic: {}", e.getMessage(), e);
}
return null;
}
/**
* 从Mapper类推断对应的实体类 优先通过泛型参数获取,如果失败则尝试命名规范推断 使用缓存机制避免重复的类加载和反射操作
*
* @param mapperClass Mapper类
* @return 推断的实体类,未找到则返回null
*/
private Class<?> getEntityClassFromMapper(Class<?> mapperClass) {
// 优先尝试从泛型参数获取实体类
Class<?> entityClass = getEntityClassFromGeneric(mapperClass);
if (entityClass != null) {
return entityClass;
}
// 如果泛型方式失败,使用命名规范推断
return getEntityClassFromNaming(mapperClass);
}
/**
* 通过命名规范推断实体类(备用方案)
* <p>
* 性能优化:使用缓存避免重复的类加载和反射操作
*
* @param mapperClass Mapper类
* @return 推断的实体类,未找到则返回null
*/
private Class<?> getEntityClassFromNaming(Class<?> mapperClass) {
String mapperClassName = mapperClass.getName();
return ENTITY_CLASS_CACHE.computeIfAbsent(mapperClassName, key -> tryFindEntityClass(mapperClassName));
}
/**
* 生成浮动过期时间,防止缓存雪崩 在默认过期时间基础上添加随机浮动时间(±10%)
*
* @param baseExpireTime 基础过期时间(秒)
* @return 浮动后的过期时间(秒)
*/
private long getFloatingExpireTime(long baseExpireTime) {
// 计算浮动范围(±10%)
long floatRange = baseExpireTime / 10;
// 生成-floatRange到+floatRange之间的随机数
long randomFloat = RANDOM.nextLong(2 * floatRange + 1) - floatRange;
return baseExpireTime + randomFloat;
}
/**
* 获取实体类中标注了@TableId注解的字段
*
* @param modelClass 实体类Class对象
* @return 主键字段,未找到则返回null
*/
private Field getIdField(Class<?> modelClass) {
for (Field field : modelClass.getDeclaredFields()) {
if (field.isAnnotationPresent(TableId.class)) {
return field;
}
}
return null;
}
/**
* 性能优化:缓存方式获取实体类的主键字段 避免重复反射操作
*
* @param modelClass 实体类Class对象
* @return 标注了@TableId的字段,未找到则返回null
*/
private Field getIdFieldCached(Class<?> modelClass) {
return ID_FIELD_CACHE.computeIfAbsent(modelClass, this::getIdField);
}
/**
* 从各种类型的参数中提取主键ID值
* <pre>
* 这是缓存系统的核心方法,支持以下参数类型:
* 1. 直接的主键值(String、Integer、Long)
* 2. Map参数(包含MyBatis-Plus的各种Wrapper和实体对象)
* 3. 实体对象(通过反射获取@TableId字段值)
* </pre>
*
* @param modelClass 实体类Class对象
* @param parameter MyBatis传入的参数对象
* @return 提取到的主键值,无法提取则返回null
*/
private Object getIdValue(Class<?> modelClass, Object parameter) {
if (parameter == null) {
return null;
}
if (log.isDebugEnabled()) {
log.debug("Getting ID value for model class: {}, parameter type: {}", modelClass.getName(), parameter.getClass()
.getName());
}
// 处理基本类型主键值(String、Integer、Long)
if (isPrimitiveType(parameter)) {
return parameter;
}
// 处理Map参数
if (parameter instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> paramMap = (Map<String, Object>) parameter;
return extractIdFromMapParameter(modelClass, paramMap);
}
// 处理实体对象
if (modelClass.isInstance(parameter)) {
return getIdValueFromEntity(modelClass, parameter);
}
if (log.isDebugEnabled()) {
log.debug("Parameter is not an instance of entity class: {}, parameter type: {}", modelClass.getName(), parameter.getClass()
.getName());
}
return null;
}
/**
* 从实体对象中获取ID值 性能优化:使用缓存的字段访问器避免重复反射操作
*
* @param modelClass 实体类Class对象
* @param entity 实体对象实例
* @return 主键值,获取失败则返回null
*/
private Object getIdValueFromEntity(Class<?> modelClass, Object entity) {
if (!modelClass.isInstance(entity)) {
return null;
}
Field idField = getIdFieldCached(modelClass);
if (idField == null) {
return null;
}
try {
// 使用缓存的字段访问器提升性能
Function<Object, Object> accessor = FIELD_ACCESSOR_CACHE.computeIfAbsent(idField, field -> {
field.setAccessible(true);
return obj -> {
try {
return field.get(obj);
} catch (IllegalAccessException e) {
log.warn("Failed to get field value: {}", e.getMessage(), e);
return null;
}
};
});
return accessor.apply(entity);
} catch (Exception e) {
log.warn("Failed to get ID from entity: {}", e.getMessage(), e);
return null;
}
}
/**
* 逐个获取Redis键值
*/
private List<Object> getIndividualKeys(List<String> redisKeys) {
List<Object> results = new ArrayList<>(redisKeys.size());
for (String key : redisKeys) {
try {
Object value = redisTemplate.opsForValue()
.get(key);
results.add(value);
} catch (Exception ex) {
log.warn("Failed to get value for key: {}, error: {}", key, ex.getMessage(), ex);
results.add(null);
}
}
return results;
}
/**
* 从MappedStatement推断对应的实体类 通过解析SQL ID获取Mapper类,然后推断实体类
*
* @param ms MyBatis的MappedStatement对象
* @return 推断的实体类,失败则返回null
*/
private Class<?> getModelClass(MappedStatement ms) {
try {
String id = ms.getId(); // 类名+方法名
String mapperClassName = id.substring(0, id.lastIndexOf('.'));
Class<?> mapperClass = Class.forName(mapperClassName);
// 从Mapper类推断实体类
return getEntityClassFromMapper(mapperClass);
} catch (Exception e) {
log.warn("Failed to get model class from MappedStatement: {}", e.getMessage(), e);
return null;
}
}
/**
* 生成Redis缓存键 格式:{包名.类名}:主键值
* <pre>
* 使用Hash Tag确保相同实体类的所有key在Redis Cluster中分配到同一槽位
* 同时避免不同包下同名类的key冲突问题
* </pre>
*
* @param clazz 实体类Class对象
* @param idValue 主键值
* @return Redis缓存键
*/
private String getRedisKey(Class<?> clazz, Object idValue) {
String fullClassName = ShortClassNameUtil.simplifyClassName(clazz.getName(), null);
// 使用Hash Tag语法 {fullClassName} 确保相同实体类的key都分配到同一个槽位
// 同时避免不同包下同名类的key冲突(如 com.au92.user.User 和 com.au92.admin.User)
return "{" + fullClassName + "}:" + idValue;
}
/**
* 处理批量查询,支持Redis mget优化
* <p>
* 通过批量获取缓存减少Redis网络交互次数,提升性能
*
* @param invocation MyBatis方法调用对象
* @param parameter 查询参数
* @param modelClass 实体类Class对象
* @return 查询结果列表
* @throws Throwable 执行过程中的异常
*/
private Object handleBatchQuery(Invocation invocation, Object parameter, Class<?> modelClass) throws Throwable {
BatchQueryContext context = prepareBatchQueryContext(parameter);
if (context == null) {
return invocation.proceed();
}
if (log.isDebugEnabled()) {
log.debug("Handling batch query for {} IDs", context.ids.size());
}
// 构建Redis keys并批量获取缓存
BatchCacheResult cacheResult = getBatchCacheResults(context.ids, modelClass);
// 处理缓存未命中的ID,查询数据库
Object dbResults = handleCacheMissedIds(invocation, parameter, cacheResult.missedIds, modelClass);
// 合并缓存和数据库结果
return mergeBatchResults(cacheResult.cachedResults, dbResults, cacheResult.missedIds, modelClass);
}
/**
* 处理缓存未命中的ID,查询数据库
*
* @param invocation MyBatis方法调用对象
* @param originalParameter 原始查询参数
* @param missedIds 未命中的ID列表
* @param modelClass 实体类Class对象
* @return 查询结果列表
* @throws Throwable 执行过程中的异常
*/
private Object handleCacheMissedIds(Invocation invocation, Object originalParameter, List<Object> missedIds, Class<?> modelClass) throws Throwable {
if (missedIds.isEmpty()) {
return new ArrayList<>();
}
if (log.isDebugEnabled()) {
log.debug("Cache miss for {} IDs, querying database", missedIds.size());
}
// 修改参数只查询未命中的ID
@SuppressWarnings("unchecked")
Map<String, Object> paramMap = (Map<String, Object>) originalParameter;
Map<String, Object> newParamMap = new HashMap<>(paramMap);
newParamMap.put("coll", missedIds);
// 创建新的调用参数
Object[] newArgs = invocation.getArgs()
.clone();
newArgs[1] = newParamMap;
// 执行数据库查询
return new Invocation(invocation.getTarget(), invocation.getMethod(), newArgs).proceed();
}
/**
* 处理单条记录查询
* <p>
* 先尝试从Redis缓存获取,缓存未命中则查询数据库并缓存结果
*
* @param invocation MyBatis方法调用对象
* @param ms MappedStatement对象
* @param parameter 查询参数
* @return 查询结果
* @throws Throwable 执行过程中的异常
*/
private Object handleQuery(Invocation invocation, MappedStatement ms, Object parameter) throws Throwable {
Class<?> modelClass = getModelClass(ms);
if (modelClass == null) {
return invocation.proceed();
}
// 检查是否是批量查询
if (isBatchQuery(ms, parameter)) {
return handleBatchQuery(invocation, parameter, modelClass);
}
return handleSingleQuery(invocation, modelClass, parameter);
}
/**
* 处理单条记录查询
*/
private Object handleSingleQuery(Invocation invocation, Class<?> modelClass, Object parameter) throws Throwable {
Object idValue = getIdValue(modelClass, parameter);
if (idValue == null) {
return invocation.proceed();
}
String redisKey = getRedisKey(modelClass, idValue);
// 尝试从缓存获取
Object cached = getCachedResult(redisKey);
if (cached != null) {
if (cached instanceof List) {
return cached;
}
return List.of(cached);
}
// 缓存未命中,查询数据库并缓存结果
return queryDatabaseAndCache(invocation, redisKey);
}
/**
* 处理更新操作(新增、修改、删除) 采用双删策略:操作前删除缓存 -> 执行数据库操作 -> 操作后再删除缓存 确保数据一致性,避免缓存和数据库数据不一致的问题
*
* @param invocation MyBatis方法调用对象
* @param ms MappedStatement对象
* @param parameter 操作参数
* @return 操作结果
* @throws Throwable 执行过程中的异常
*/
private Object handleUpdate(Invocation invocation, MappedStatement ms, Object parameter) throws Throwable {
Class<?> modelClass = getModelClass(ms);
if (modelClass == null) {
return invocation.proceed();
}
String redisKey = "";
Object idValue = getIdValue(modelClass, parameter);
if (idValue != null) {
redisKey = getRedisKey(modelClass, idValue);
redisTemplate.delete(redisKey);
if (log.isDebugEnabled()) {
log.debug("Deleted cache for key: {}", redisKey);
}
} else {
log.warn("NOT Deleted cache for parameter: {}", JsonUtils.toJSONString(parameter));
}
Object result = invocation.proceed();
if (StringUtils.isNotBlank(redisKey)) {
// double delete cache
redisTemplate.delete(redisKey);
}
return result;
}
/**
* 检查类是否有@TableId注解的字段 用于验证类是否为有效的MyBatis-Plus实体类
*
* @param clazz 要检查的类
* @return true表示有@TableId字段,false表示没有
*/
private boolean hasTableIdField(Class<?> clazz) {
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(TableId.class)) {
return true;
}
}
return false;
}
/**
* 检查是否是批量查询(如selectByIds) 通过方法名和参数类型判断是否为批量查询
*
* @param ms MappedStatement对象
* @param parameter 查询参数
* @return true表示是批量查询,false表示不是
*/
private boolean isBatchQuery(MappedStatement ms, Object parameter) {
String sqlId = ms.getId();
// 检查方法名是否包含批量查询的关键词
if (sqlId.contains("selectByIds") || sqlId.contains("listByIds") || sqlId.contains("selectBatchIds") || sqlId.contains("selectByBatchIds")) {
return true;
}
// 检查参数是否是Collection类型
if (parameter instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> paramMap = (Map<String, Object>) parameter;
// MyBatis-Plus批量查询通常包含coll参数
return paramMap.containsKey("coll") && paramMap.get("coll") instanceof java.util.Collection;
}
return false;
}
/**
* 检查是否为复杂查询(包含AND、OR的多条件查询)
*/
private boolean isComplexQuery(String normalizedSql) {
return normalizedSql.contains(" and ") || normalizedSql.contains(" or ");
}
/**
* 判断查询结果是否为空
*/
private boolean isEmptyResult(Object result) {
return result == null || (result instanceof Collection && ((Collection<?>) result).isEmpty());
}
/**
* 判断参数是否为基本类型的主键值
*/
private boolean isPrimitiveType(Object parameter) {
return parameter instanceof String || parameter instanceof Integer || parameter instanceof Long;
}
/**
* 判断是否为简单的主键等值查询
*
* @param normalizedSql 规范化的SQL片段
* @param idColumnLower 小写的主键列名
* @return true表示是简单主键查询
*/
private boolean isSimpleIdQuery(String normalizedSql, String idColumnLower) {
// 标准格式: id = ?
if (normalizedSql.equals(idColumnLower + " = ?")) {
return true;
}
// MyBatis-Plus格式: id = #{ew.paramNameValuePairs.MPGENVAL1}
if (normalizedSql.startsWith(idColumnLower + " = #{") && normalizedSql.endsWith("}")) {
String paramPattern = idColumnLower + " = #{";
return normalizedSql.equals(paramPattern + normalizedSql.substring(paramPattern.length()));
}
return false;
}
/**
* 验证是否为简单的主键查询
*
* @param sqlSegment SQL片段,通常是Wrapper的getSqlSegment()返回值
* @param modelClass 实体类Class对象
* @return 如果是简单的主键等值查询则返回true,否则返回false
*/
private boolean isSimpleIdWrapperQuery(String sqlSegment, Class<?> modelClass) {
// 获取ID字段信息
Field idField = getIdFieldCached(modelClass);
if (idField == null) {
return false;
}
String idColumnName = getColumnName(idField);
if (log.isDebugEnabled()) {
log.debug("ID column name: {}", idColumnName);
}
// 规范化SQL片段
String normalizedSql = normalizeSqlSegment(sqlSegment);
String idColumnLower = idColumnName.toLowerCase();
// 检查是否为复杂查询
if (isComplexQuery(normalizedSql)) {
if (log.isDebugEnabled()) {
log.debug("Skipping cache for complex query with multiple conditions: {}", normalizedSql);
}
return false;
}
// 判断是否为简单的主键等值查询
boolean isSimpleIdEqualsQuery = isSimpleIdQuery(normalizedSql, idColumnLower);
if (!isSimpleIdEqualsQuery) {
if (log.isDebugEnabled()) {
log.debug("Skipping cache for non-simple ID query: {}", normalizedSql);
}
}
return isSimpleIdEqualsQuery;
}
/**
* 合并批量查询的缓存和数据库结果
*
* @param cachedResults 批量缓存命中结果列表
* @param dbResults 数据库查询结果
* @param missedIds 批量缓存未命中的ID列表
* @param modelClass 实体类Class对象
* @return 合并缓存和数据库查询的结果后的结果列表
*/
private Object mergeBatchResults(List<Object> cachedResults, Object dbResults, List<Object> missedIds, Class<?> modelClass) {
List<Object> finalResults = new ArrayList<>(cachedResults);
Map<Object, Object> resultMap = new HashMap<>();
// 将缓存结果放入resultMap
for (Object cached : cachedResults) {
if (cached != null) {
Object id = getIdValueFromEntity(modelClass, cached);
if (id != null) {
resultMap.put(id, cached);
}
}
}
if (dbResults instanceof List<?> dbResultList) {
// 缓存数据库查询结果
cacheDatabaseResults(dbResultList, modelClass, finalResults, resultMap);
// 对于数据库中也没有的ID,缓存NULL_VALUE
cacheNullResults(missedIds, resultMap, modelClass);
}
if (log.isDebugEnabled()) {
log.debug("Batch query completed: {} total results, {} from cache, {} from database", finalResults.size(), finalResults.size() - missedIds.size(), missedIds.size());
}
return finalResults;
}
/**
* Redis Cluster兼容的批量获取缓存方法
* <p>
* 当Redis配置为Cluster模式时,multiGet要求所有key在同一槽位,否则会报错
* <p>
* 通过Hash Tag语法确保同一实体类的key在同一槽位,使批量操作正常工作
*
* @param redisKeys 要获取的Redis key列表
* @return 对应的值列表,顺序与key列表一致,未找到的值为null
*/
private List<Object> multiGetClusterCompatible(List<String> redisKeys) {
if (redisKeys.isEmpty()) {
return new ArrayList<>();
}
try {
// 尝试使用标准的multiGet,如果所有key都在同一槽位则正常执行
return redisTemplate.opsForValue()
.multiGet(redisKeys);
} catch (Exception e) {
// 如果multiGet失败(通常是因为key分布在不同槽位),则回退到逐个获取
log.warn("multiGet failed, keys:[{}] falling back to individual gets: {}", redisKeys, e.getMessage(), e);
return getIndividualKeys(redisKeys);
}
}
/**
* 规范化SQL片段用于匹配判断
*
* @param sqlSegment 原始SQL片段
* @return 规范化后的SQL片段
*/
private String normalizeSqlSegment(String sqlSegment) {
String normalizedSql = sqlSegment.toLowerCase()
.trim();
// 移除外层括号
if (normalizedSql.startsWith("(") && normalizedSql.endsWith(")")) {
normalizedSql = normalizedSql.substring(1, normalizedSql.length() - 1)
.trim();
}
return normalizedSql;
}
/**
* 准备批量查询上下文
*/
private BatchQueryContext prepareBatchQueryContext(Object parameter) {
if (!(parameter instanceof Map)) {
return null;
}
@SuppressWarnings("unchecked")
Map<String, Object> paramMap = (Map<String, Object>) parameter;
Object coll = paramMap.get("coll");
if (!(coll instanceof Collection<?> ids) || ids.isEmpty()) {
return null;
}
return new BatchQueryContext(ids);
}
/**
* 处理批量缓存结果
*
* @param redisKeys Redis缓存键列表
* @param cachedResults 批量获取的缓存结果列表
* @param keyToIdMap 键到ID的映射
* @return 批量缓存结果对象,包含命中结果和未命中的ID列表
*/
private BatchCacheResult processBatchCacheResults(List<String> redisKeys, List<Object> cachedResults, Map<String, Object> keyToIdMap) {
List<Object> finalResults = new ArrayList<>();
List<Object> missedIds = new ArrayList<>();
for (int i = 0; i < redisKeys.size(); i++) {
String redisKey = redisKeys.get(i);
Object cached = cachedResults.get(i);
Object id = keyToIdMap.get(redisKey);
if (cached != null) {
if (!NULL_VALUE_PLACEHOLDER.equals(cached)) {
finalResults.add(cached);
if (log.isDebugEnabled()) {
log.debug("Cache hit for batch query, key: {}", redisKey);
}
}
// NULL_VALUE表示数据库中确实没有这条记录,不需要再查询
} else {
// 缓存未命中,需要查询数据库
missedIds.add(id);
}
}
return new BatchCacheResult(finalResults, missedIds);
}
/**
* 查询数据库并缓存结果
*/
private Object queryDatabaseAndCache(Invocation invocation, String redisKey) throws Throwable {
// 缓存未命中,继续执行数据库查询
Object result = invocation.proceed();
if (isEmptyResult(result)) {
// 缓存空值,避免频繁查询数据库
cacheNullValue(redisKey);
return result;
}
// 缓存查询结果
cacheQueryResult(redisKey, result instanceof List ? ((List<?>) result).getFirst() : result);
return result;
}
/**
* 尝试根据命名规范查找实体类
*/
private Class<?> tryFindEntityClass(String mapperClassName) {
String entityName = extractEntityName(mapperClassName);
String packagePrefix = extractPackagePrefix(mapperClassName);
String[] possiblePaths = buildPossibleEntityPaths(packagePrefix, entityName);
return findValidEntityClass(possiblePaths, mapperClassName);
}
/**
* 批量查询上下文
*
* @param ids 批量查询的ID集合
*/
private record BatchQueryContext(Collection<?> ids) {
}
/**
* 批量缓存结果
*
* @param cachedResults 批量缓存命中结果列表
* @param missedIds 批量缓存未命中的ID列表
*/
private record BatchCacheResult(List<Object> cachedResults, List<Object> missedIds) {
}
}
package com.au92.common.util.exception;
import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.StringUtils;
/**
* 更短的类名
*
* @author p_x_c
*/
@UtilityClass
public class ShortClassNameUtil {
// 白名单,不需要简化的类名
public static final String PACKAGE_NAME = "com.au92";
/**
* 简化类名
*
* @param fullClassName 完整类名
* @return 简化后的类名
*/
public String simplifyClassName(String fullClassName) {
return simplifyClassName(fullClassName, PACKAGE_NAME);
}
/**
* 简化类名
*
* @param fullClassName 完整类名
* @param allowPackageName 允许的包名,如果类名包含该包名,则不进行简化
* @return 简化后的类名
*/
public String simplifyClassName(String fullClassName, String allowPackageName) {
if (StringUtils.isNotBlank(allowPackageName) && fullClassName.contains(allowPackageName)) {
return fullClassName;
}
String[] parts = fullClassName.split("\\.");
StringBuilder simplifiedName = new StringBuilder();
for (int i = 0; i < parts.length - 1; i++) {
simplifiedName.append(parts[i].charAt(0))
.append('.');
}
simplifiedName.append(parts[parts.length - 1]);
return simplifiedName.toString();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment