项目开发中往往会遇到一些查询逻辑较为复杂的报表,这些查询耗时动辄几十秒,甚至是几分钟,并且分页或排序时,往往是重新执行一遍SQL,效率低下。针对此情况,使用缓存能的解决例如排行榜和报表以及一些一致性要求不强的数据,并且对缓存数据结构的设计,可以实现对缓存数据的排序和分页功能,解决分页和排序时重新执行SQL的问题。
目的:
1)缓存SQL查询结果。
2)分页和排序通过Redis进行操作,减少数据库的查询次数。
环境:
工具:Redis 4.0.6,Jedis 2.1.0
平台:Java 1.7
数据库:MySql 5.7.17
实现:
1、生成key策略:
目的是确保唯一性,笔者采用 “方法名” + “SQL”,也可以视情况加上时间戳等条件。
2、工具类:
序列化工具。Redis不支持Java将对象直接存储到数据库中,因此需要将Java对象进行序列化,反之从Redis中取出也要反序列化。
public byte[] serialize(Object object) {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
return baos.toByteArray();
}catch (Exception e) {
throw new CacheException(e);
}
}
public Object unserialize(byte[] bytes) {
if (bytes == null) {
return null;
}
ByteArrayInputStreambais = null;
try {
bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
}catch (Exception e) {
throw new CacheException(e);
}
}
分页工具类
public class PageData<T> {
/** 数据集合 */
protected List<T> result = Lists.newArrayList();
/** 数据总数 */
protected int totalCount = 0;
/** 总页数 */
protected long pageCount = 0;
/** 每页记录 */
protected int pageSize = 15;
/** 初始当前页 */
protected int pageNo = 1;
}
3、缓存结构设计
缓存数据这里要分3种类型:需要分页的数据、需要分页和排序的数据和普通数据。
3.1普通数据(既不需要分页也不需要排序)
存储到Hash数据中
public void putObject(final Object key,final Object value, final Integer timeout) {
// 全局变量Hash表的ID
finalbyte[] idBytes = id.getBytes();
Jedisjedis = jedisPool.getResource();
jedis.hset(idBytes,key.toString().getBytes(), SerializeUtil.serialize(value));
if(timeout != null && jedis.ttl(idBytes) == -1) {
jedis.expire(idBytes,timeout);
}
}
public Object getObject(final Object key) {
Jedis jedis =jedisPool.getResource();
return SerializeUtil.unserialize(jedis.hget(id.getBytes(),key.toString().getBytes()));
}
3.2有分页需求的数据
MySql中Limit分页是将所有符合条件的数据全部查询出来,再根据Limit的参数进行截取。
Redis中Sorted Set可以很好的完成分页的功能。将查询结果全部存储到缓存中,每次获取分页数据,都由Redis完成。
Sorted Set API:
获得总记录数:zcard(key)
获得指定范围的结果集:zrange(key, beginIndex, endIndex)
缓存结果图解:
(Member记录每行结果,用逗号分隔每一列,分数记录每行结果的索引值。)
/**
* 保存缓存
* @param key 根据策略生成的key
* @param list 查询结果
* @param timeout 缓存过期时间
*/
public void saveCache(final String key, List<?> list, final Integer timeout) {
if (list.size() > 0) {
Jedis redis = jedisPool.getResource();
Pipeline pipeline = redis.pipelined();
// 开启事务
pipeline.multi();
for (Integer i = 0; i < list.size(); i++) {
Object[] dataForRow = (Object[])list.get(i);
String setValue = "";
for (Integer j = 0; j < dataForRow.length; j++) {
if (dataForRow[j] == null) {
dataForRow[j] = " ";
}
setValue += dataForRow[j].toString() + ",";
}
pipeline.zadd(key, (double)i, setValue);
}
// 设置过期时间
pipeline.expire(key, timeout);
// 提交事务
pipeline.exec();
pipeline.sync();
jedisPool.returnResource(redis);
}
}
/**
* 从缓存读取数据
* @param pageData 分页工具类
* @param key
* @return
*/
public PageData<Object[]> findPageDataByCache(PageData<Object[]> pageData, final String key) {
Jedis redis = jedisPool.getResource();
List<Object[]> cacheList = new ArrayList<Object[]>();
if (key!= null && !"".equals(key)) {
// 获得总记录数
Long totalCount = redis.zcard(key);
if (totalCount > 0) {
// 计算分页
Integer beginIndex = ((pageData.getPageNo() - 1) * pageData.getPageSize());
Integer endIndex = (beginIndex + pageData.getPageSize() - 1);
if (pageData.getTotalCount() == 0) {
// 保存总记录数
pageData.setTotalCount(totalCount.intValue());
}
// Sorted Set返回结果集封装为LinkedHashSet
Set<String> cacheDataForRow = redis.zrange(key, beginIndex, endIndex);
for (String dataUnitArray : cacheDataForRow) {
Object[] dataUnit = (Object[])dataUnitArray.split(",");
cacheList.add(dataUnit);
}
pageData.setResult(cacheList);
}
}
jedisPool.returnResource(redis);
return pageData;
}
3.3有分页和排序需求的数据
设计原理:
根据存储分页数据的基础上,每一行生成一条Hash结构的数据。
排序需要建立列名和对应列的数据的对应关系,即获得排序字段时,能找到该列的所有数据才能进行比较。
Hash数据的key根据Sorted Set的分数(即每行数据的索引值) + Key生成,这样做是为了根据每行的索引值找到该set生成的所有的Hash数据。
根据某列进行排序,得到排序后行索引值的集合,再根据索引值取出Sorted Set中的数据。使用Redis的Sort命令实现此需求。
图解:
现在对row0做降序处理,row0中的3条数据的值是4,1,7,因此我们希望得到的索引集合是2,1,0。
存储每一行列名和值的Hash数据
Sorted Set数据
indexList存储是原Sorted Set中的索引号,即0,1,2
Sort命令得到排序后的索引,通过Hash数据的row0列进行排序,返回indexList中的值。
其中BY *key1->row0:
*通配符是指从indexList中取出的数值,key1是缓存数据的key;*key1相当于生成0key1、1key1、2key1。
->row0是指以“*key1”的Hash数据中取”row0”列进行比较、排序。
SORT indexList BY *key1->row0 DESC : 以0key1、1key1、2key1中field是row0的值组合成的新key,并按照新key中的内容进行排序,返回排序后的indexList。
注意:若排序字段是字符串,需要加ALPHA参数。Sort命令的详细使用方法可查看官网文档。
有了排序后的索引值集合,就可以取出排序后的结果集。
代码部分:
注意:根据SQL生成Key时不要将order参数写入,会导致每次查询排序时生成的Key不相同,将order作为参数交给Redis进行操作。
/**
* 生成排序索引key
*/
public static synchronized String createSortedListKey(String conditionId, String sortName, String sortOrder) {
String sortedListKey = conditionId + ":" + sortName + ":" + sortOrder;
return sortedListKey;
}
/**
* 生成排序索引key
*/
public static synchronized String createSortedListKey(String conditionId, String sortName, String sortOrder) {
String sortedListKey = conditionId + ":" + sortName + ":" + sortOrder;
return sortedListKey;
}
/**
* 保存缓存(排序)
* @param list
* @param rowNameList 列名集合
* @param timeout
*/
public void saveCacheDataWithSort(List<?> list, List<String> rowNameList , final String key, final Integer timeout) {
if (list.size() > 0) {
Jedis redis = jedisPool.getResource();
Pipeline pipeline = redis.pipelined();
// 开启事务
pipeline.multi();
for (Integer i = 0; i < list.size(); i++) {
Object[] dataForRow = (Object[])list.get(i);
String setValue = "";
Map<String, String> resultWithRowMap = new HashMap<String, String>();
for (Integer j = 0; j < dataForRow.length; j++) {
if (dataForRow[j] == null) {
dataForRow[j] = " ";
}
resultWithRowMap.put(rowNameList.get(j), dataForRow[j].toString());
setValue += dataForRow[j].toString() + ",";
}
// setValueMap.put((double)i, setValue);
pipeline.zadd(key, (double)i, setValue);
// 将每一行数据(含列名)保存hash数据
pipeline.hmset(i + key, resultWithRowMap);
pipeline.expire(i + key, timeout);
}
// 设置过期时间
pipeline.expire(key, timeout);
// 提交事务
pipeline.exec();
pipeline.sync();
jedisPool.returnResource(redis);
}
}
/**
* 读取缓存返回pageData分页对象(含排序)
* @param pageData
* @param sortName
* @param sortOrder
* @param sortIsString 排序字段是否字符
* @return
*/
public PageData<Object[]> findPageDataByCacheWithSort(PageData<Object[]> pageData, final String key, String sortName, String sortOrder, Boolean sortIsString) {
// 生成排序索引key,缓存Sort命令排序后生成的索引集合
String sortedListKey = CacheUtil.createSortedListKey(key, sortName, sortOrder);
Jedis redis = jedisPool.getResource();
List<Object[]> cacheList = new ArrayList<Object[]>();
if (key!= null && !"".equals(key)) {
Integer totalCount = redis.zcard(key).intValue();
if (totalCount > 0) {
// 查看是是否有该排序的缓存
byte[] bytes = redis.get(sortedListKey.getBytes());
List<?> resultIndexList = SerializeUtil.unserializeList(bytes);
if (resultIndexList == null || resultIndexList.isEmpty()) {
// 根据缓存数据的长度,创建用于存储每行数据索引的临时List
String uuId = UUID.randomUUID().toString().replaceAll("-", "");
Pipeline pipeline = redis.pipelined();
for (Integer i = 0; i < totalCount; i++) {
pipeline.rpush(uuId, i.toString());
}
pipeline.sync();
// 排序参数对象
SortingParams sortingParams = new SortingParams();
// *是通配符,此处zset中所有分数
// *zsetId取出所有行数据的hash数据
// ->sortName指以hash数据中的sortName字段进行排序
sortingParams.by("*" + key + "->" + sortName);
if ("asc".equals(sortOrder)) {
sortingParams.asc();
} else {
sortingParams.desc();
}
if (sortIsString) {
// 根据字符进行排序
sortingParams.alpha();
}
// 获得排序后的索引集合
resultIndexList = redis.sort(uuId, sortingParams);
// 删除key = uuId的临时List
redis.ltrim(uuId, 1, 0);
// 将排序后的索引存入缓存,过期时间与当前报表同步
redis.set(sortedListKey.getBytes(), SerializeUtil.serializeList(resultIndexList));
redis.expire(sortedListKey, redis.ttl(key).intValue());
}
// 根据pageData属性计算分页
Integer beginIndex = ((pageData.getPageNo() - 1) * pageData.getPageSize());
Integer endIndex = (beginIndex + pageData.getPageSize());
if (pageData.getTotalCount() == 0) {
// 保存总记录数
pageData.setTotalCount(totalCount);
}
// 若取值范围大于实际结果集长度,则endIndex取实际结果集长度
endIndex = resultIndexList.size() < endIndex ? resultIndexList.size() : endIndex;
// 根据排序索引从缓存中逐条读取
Pipeline pipeline2 = redis.pipelined();
List<Response<Set<String>>> responseSetList = new ArrayList<Response<Set<String>>>();
for (Integer j = beginIndex; j < endIndex; j++) {
String index = resultIndexList.get(j).toString();
Response<Set<String>> responseSet = pipeline2.zrangeByScore(key, index, index);
responseSetList.add(responseSet);
}
pipeline2.sync();
for (Response<Set<String>> response : responseSetList) {
Set<String> cacheDataSet = response.get();
Iterator<String> it = cacheDataSet.iterator();
if (it.hasNext()) {
Object[] dataUnit = (Object[])it.next().split(",");
cacheList.add(dataUnit);
}
}
pageData.setResult(cacheList);
}
}
jedisPool.returnResource(redis);
return pageData;
}
第一次写博客,内容很粗糙,仅供参考;欢迎各种建议、指正和交流。
邮箱:594187062@qq.com