Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.kould.manager.impl.RedisCacheManager Maven / Gradle / Ivy
package com.kould.manager.impl;
import com.kould.properties.DaoProperties;
import com.kould.properties.DataFieldProperties;
import com.kould.api.Kache;
import com.kould.encoder.CacheEncoder;
import com.kould.entity.NullValue;
import com.kould.manager.RemoteCacheManager;
import com.kould.entity.MethodPoint;
import com.kould.service.RedisService;
import com.kould.utils.FieldUtils;
import io.lettuce.core.*;
import io.lettuce.core.api.sync.RedisCommands;
import java.lang.reflect.Field;
import java.util.*;
/*
使用Redis进行缓存存取的CacheManager
通过Lua脚本进行主要的存取实现
实现散列化的形式进行缓存存储
*/
public class RedisCacheManager extends RemoteCacheManager {
private static final String INDEX_TAG_KEY = Kache.CACHE_PREFIX + Kache.INDEX_TAG;
private static final Object COLLECTION_KRYO = new ArrayList<>();
//Lua脚本,用于在Redis中通过Redis中的索引收集获取对应的散列PO类
//需要优化Kache.SERVICE_BY_FIELD的查找,使用索引优化
private static final String SCRIPT_LUA_CACHE_GET =
"local keys = redis.call('lrange',KEYS[1],0,-1) " +
"local keySize = table.getn(keys) " +
// 此处可能会因为增删改导致元缓存被删除
// 若元缓存与索引id集内数量不匹配则返回Null并从数据库获取最新数据
"for i=2, keySize do " +
" local value = redis.call('get',keys[i]) " +
" if(value ~= nil) " +
" then " +
" keys[i] = value " +
" else " +
" return nil " +
" end " +
"end " +
"return keys ";
private String scriptGetSHA1 ;
private final RedisService redisService;
private final CacheEncoder cacheEncoder;
public RedisCacheManager(DataFieldProperties dataFieldProperties, DaoProperties daoProperties, RedisService redisService, CacheEncoder cacheEncoder) {
super(dataFieldProperties, daoProperties);
this.redisService = redisService;
this.cacheEncoder = cacheEncoder;
}
private static final String CAS_NULL = "$1";
/**
* 初始化预先缓存对应Lua脚本并取出脚本SHA1码存入变量
*/
@Override
public void init() throws RuntimeException {
redisService.executeSync(commands -> {
scriptGetSHA1 = commands.scriptLoad(SCRIPT_LUA_CACHE_GET);
return true;
});
}
@Override
public String getNullTag() {
return "To The Moon";
}
/**
* 以拼接lua语句脚本的形式多次提交,规则为:
* 索引:以list的形式存入,以result的类型做多种处理
* 1、若result为Collection或包装类:
* (若为包装类会提前提出其中的保存PO数据的属性与Collection同样处理)
* 将数据逐条获取id当作key,数据为value,以此对齐分别存入id集合和具体数据集合中
* (keys和values则为Lua脚本对参数的包装);
* 并将首条数据将Collection或包装类除去真实数据保存其状态;
* 最后将参数key作为key和上述的id集作为value组成一条list存入,而每条id则分别与对应的PO进行存入
* 2、若result为一条POBean:
* 则将直接获取result的id并与result作为单条PO存入;
* 再将key和id作为键值对,存入作为索引
* @param key 索引值
* @param type 主元缓存类型名
* @param point 切入点
* @return 存入成功返回传入的result,失败则返回null
*/
@Override
public Object put(String key, String type, MethodPoint point) throws RuntimeException {
return redisService.executeSync(commands -> {
String recordsName = dataFieldProperties.getRecordsName();
Object result = point.execute();
if (result instanceof Collection) {
collection2Lua(commands, key, type, (Collection) result,null);
} else if (result != null && isHasField(result.getClass(), recordsName)) {
Field recordsField = FieldUtils.getFieldByNameAndClass(result.getClass(), recordsName);
Collection records = (Collection) recordsField.get(result) ;
recordsField.set(result,Collections.emptyList());
collection2Lua(commands, key, type, records, result);
recordsField.set(result,records);
} else {
noPackingResultPut(key, type, commands, result);
}
return result ;
});
}
/**
* 非包装或集合数据缓存处理方法
* @param key 缓存key
* @param type 缓存实体类类型
* @param commands Redis操作对象
* @param result 数据库结果
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private void noPackingResultPut(String key, String type, RedisCommands commands, Object result) throws NoSuchFieldException, IllegalAccessException {
StringBuilder lua = new StringBuilder();
lua.append("redis.call('setex',KEYS[1],")
.append(daoProperties.getCacheTime())
.append(",ARGV[1]);");
String[] keys = new String[2];
Object[] values = new Object[2];
if (!key.contains(INDEX_TAG_KEY)) {
//若为ID方法,则直接将key赋值给id
keys[0] = cacheEncoder.getId2Key(key, type);
} else if (result != null) {
//获取条件方法单结果
Field primaryKeyField = FieldUtils.getFieldByNameAndClass(result.getClass()
, dataFieldProperties.getPrimaryKeyName());
keys[0] = cacheEncoder.getId2Key((String) primaryKeyField.get(result), type);
} else {
//通过将类型设为null使NullTag被通用
keys[0] = cacheEncoder.getId2Key(getNullTag(), null);
}
if (result == null) {
values[0] = NullValue.getInstance();
} else {
values[0] = result;
}
//判断此时是否为id获取的单结果或者为条件查询获取的单结果
if (!key.equals(keys[0])) {
keys[1] = key;
values[1] = keys[0];
lua.append("redis.call('del',KEYS[2]);");
lua.append("redis.call('lpush',KEYS[2],ARGV[2]);") ;
lua.append("return redis.call('expire',KEYS[2],")
.append(daoProperties.getCacheTime())
.append(");");
}
commands.eval(lua.toString(), ScriptOutputType.MULTI, keys, values) ;
}
/**
* 数据集缓存解析处理
* @param commands redis操作对象
* @param key 缓存key
* @param type 缓存实体类型
* @param records 数据库返回的数据集
* @param page 包装对象
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private void collection2Lua(RedisCommands commands, String key, String type
, Collection records , Object page) throws NoSuchFieldException, IllegalAccessException {
StringBuilder idsNum = new StringBuilder();
StringBuilder lua = new StringBuilder();
//用于收集哪些元数据是否已存在(每个元素指的是records中的元素索引)
List echoIds ;
Integer count = 0;
int delCount = 0;
String[] keys = new String[records.size() + 1];
if (records.size() > 1) {
echoIds = echoRedundancyIDCache(commands, type, records, keys);
int used = 0;
int valuesSize = echoIds.size() + keys.length;
Object[] values = new Object[valuesSize];
for (Object next : records) {
count++;
//保证echoIds的总数大于于used,并筛选其中Redis没有的数据加以存储到Redis之中
if (echoIds.size() > used && echoIds.get(used) == count.longValue()) {
//单条数据缓存
lua.append("redis.call('setex',KEYS[").append(count).append("],")
.append(daoProperties.getCacheTime())
.append(",ARGV[")
.append(count - delCount)
.append("]);");
values[used] = next;
used++;
} else {
delCount++;
}
//拼接id聚合参数
idsNum.append(",ARGV[").append(count + echoIds.size()).append("]");
}
idsNum.append(",ARGV[").append(++count + echoIds.size()).append("]");
//聚合缓存
lua.append("redis.call('del',KEYS[")
.append(records.size() + 1)
.append("]);");
lua.append("redis.call('lpush',KEYS[")
.append(records.size() + 1)
.append("]")
.append(idsNum).append(");");
lua.append("return redis.call('expire',KEYS[")
.append(records.size() + 1)
.append("],")
.append(daoProperties.getCacheTime())
.append(");");
if (keys.length - 1 >= 0) {
System.arraycopy(keys, 0, values, used, keys.length - 1);
}
if (page != null) {
values[valuesSize - 1] = page;
} else {
values[valuesSize - 1] = COLLECTION_KRYO;
}
keys[keys.length - 1] = key;
commands.eval(lua.toString(), ScriptOutputType.MULTI, keys, values);
} else {
lua.append("redis.call('del',KEYS[1]) ");
lua.append("redis.call('lpush',KEYS[1],ARGV[1]) ") ;
lua.append("return redis.call('expire',KEYS[1],")
.append(daoProperties.getCacheTime())
.append(");");
keys[0] = key;
commands.eval(lua.toString(), ScriptOutputType.MULTI, keys, page);
}
}
/**
* 非冗余元缓存查询方法
*
* 生成Echo脚本(返回Redis内不存在的单条数据,用于减少重复的数据,减少重复序列化和多余的IO占用)
* @param commands redis命令操作对象
* @param type 缓存实体类型
* @param records 数据库返回的数据集(若为Page等包装类则提取Record)
* @param keys 存入缓存的Key数组
* @return 返回非冗余的id的索引List
* @throws IllegalAccessException
* @throws NoSuchFieldException
*/
private List echoRedundancyIDCache(RedisCommands commands, String type, Collection records, String[] keys)
throws IllegalAccessException, NoSuchFieldException {
Iterator iterator = records.iterator();
StringBuilder echo = new StringBuilder() ;
List echoIds;
int count2Echo = 0 ;
echo.append("local result = {} ") ;
while(iterator.hasNext()) {
Object next = iterator.next();
count2Echo ++ ;
keys[count2Echo - 1] = cacheEncoder
.getId2Key(FieldUtils.getFieldByNameAndClass(next.getClass(), dataFieldProperties.getPrimaryKeyName())
.get(next).toString(), type);
echo.append("if(redis.call('EXISTS',KEYS[")
.append(count2Echo)
.append("]) == 0) ")
.append("then ")
//若不存在即加入result中被返回
.append("table.insert(result,")
.append(count2Echo)
.append(") ")
.append("else ")
//若存在则延长命中缓存的存活时间
.append("redis.call('expire',KEYS[").append(count2Echo).append("],")
.append(daoProperties.getCacheTime()).append("); ")
.append("end ") ;
}
echo.append("return result ") ;
String[] echoKeys = new String[records.size()];
System.arraycopy(keys,0,echoKeys,0, records.size());
echoIds = commands.eval(echo.toString(), ScriptOutputType.MULTI, echoKeys);
return echoIds;
}
/**
* 使用Lua脚本在Redis处直接完成索引值的散列数据收集并以List的形式返回
* 随后解析list:
* 若list的首位数据为"[]"或"{}"则说明为Collection(目前还只是支持List)
* 则直接生成一个ArrayList records并遍历list首位的以后的数据并反序列化加入到records中
* 若List的大小为1,则说明为单条PO
* 则直接反序列化为Bean并返回
* 若以上条件都不满足则说明为包装类
* 应首条数据的反序列化,得到其状态后再如同Collection的处理
* 填充其中配置文件所设置的对应数据集属性
* @param key 索引值
* @return 成功返回对应Key的结果,失败则返回null
*/
@Override
public Object get(String key) throws RuntimeException {
return redisService.executeSync(commands -> {
//判断是否为直接通过ID获取单条方法
if (!key.contains(INDEX_TAG_KEY)) {
return commands.get(key);
} else {
//为条件查询方法
List list = commands.evalsha(scriptGetSHA1, ScriptOutputType.MULTI, key);;
List records = new ArrayList<>();
if (list != null && !list.isEmpty()) {
Object first = list.get(0);
//判断结果是否为单个POBean
if (list.size() == 1) {
return list.get(0);
//判断返回结果是否为Collection或其子类
} else if (first instanceof Collection) {
//跳过第一位数据的填充
for (int i = 1; i < list.size(); i++) {
records.add(list.get(i));
}
//由于Redis内list的存储是类似栈帧压入的形式导致list存取时倒叙,所以此处取出时将顺序颠倒回原位
Collections.reverse(records);
return records;
} else {
//此时为包装类的情况
FieldUtils.getFieldByNameAndClass(first.getClass(), dataFieldProperties.getRecordsName())
.set(first, records);
//跳过第一位数据的填充
for (int i = 1; i < list.size(); i++) {
records.add(list.get(i));
}
//由于Redis内list的存储是类似栈帧压入的形式导致list存取时倒叙,所以此处取出时将顺序颠倒回原位
Collections.reverse(records);
return first;
}
} else return null;
}
}) ;
}
@Override
public boolean cas(String key) throws RuntimeException {
return redisService.executeSync(commands -> {
if (Boolean.TRUE.equals(commands.setnx(key,CAS_NULL))) {
commands.expire(key, daoProperties.getCasKeepTime());
return true;
} else {
return false;
}
});
}
@Override
public Long delKeys(String pattern) throws RuntimeException {
return redisService.executeSync(commands -> {
// SCAN参数
ScanArgs scanArgs = ScanArgs.Builder.limit(500).match(pattern);
// TEMP游标
ScanCursor cursor = ScanCursor.INITIAL;
long counter = 0L;
do {
KeyScanCursor result = commands.scan(cursor, scanArgs);
// 重置TEMP游标
cursor = ScanCursor.of(result.getCursor());
cursor.setFinished(result.isFinished());
List values = result.getKeys();
if (!values.isEmpty()) {
commands.del(values.toArray(new String[values.size()]));
}
counter++;
} while (!(ScanCursor.FINISHED.getCursor().equals(cursor.getCursor()) && ScanCursor.FINISHED.isFinished() == cursor.isFinished()));
return counter;
});
}
@Override
public Long del(String... keys) throws RuntimeException {
return redisService.executeSync(commands -> commands.del(keys));
}
/**
* 通过获取该Class的声明对象是否能够成功来判断其是否存在该属性
* @param clazz 目标Class
* @param field 属性名
* @return 存在返回true 不存在返回false
*/
private boolean isHasField(Class> clazz, String field) {
try {
FieldUtils.getFieldByNameAndClass(clazz, field);
return true;
} catch (NoSuchFieldException e) {
return false ;
}
}
}