本人业务开发中遇到了某种需要纯redis做分页查询的操作,但网上百度之后发现千篇一律,完全没有任何系统化东西呈现。故记录下自己想到的处理方式和代码实现。
ps:写的不好勿喷,实在是并没有找到对本人实用的东西。
需要实现收藏功能,或类似操作连贯性较高的非幂等性操作,且并发较高:
/**
* @author cy
* @date 2022/5/26 10:15
*/
public class RedisPage<T> {
/**
* 当前页
*/
private int current = 1;
/**
* 总数
*/
private long total;
/**
* 每页条数
*/
private int size = 5;
/**
* 总页数
*/
private long totalSize;
/**
* 数据
*/
private T data;
@Override
public String toString() {
return "RedisPage{" +
"current=" + current +
", total=" + total +
", size=" + size +
", data=" + data +
'}';
}
/**
* @description 获取当前页
* @param
* @return
*/
public int getCurrent() {
return current;
}
/**
* @description 设置当前页
* @param
* @return
*/
public void setCurrent(int current) {
if (current > 1) {
this.current = current;
}
}
/**
* @description 获取数据总条数
* @param
* @return
*/
public long getTotal() {
return total;
}
/**
* @description 设置总页数
* @param
* @return
*/
public void setTotal(long total) {
this.total = total;
}
public int getSize() {
return size;
}
/**
* @description 设置每页条数 大于等于1小于等于50才进行设置,默认5
* @param
* @return
*/
public void setSize(int size) {
int minSize = 1, maxSize = 50;
if (size >= minSize && size <= maxSize) {
this.size = size;
}
}
/**
* @description 业务数据
* @param
* @return
*/
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
/**
* @description redis分页的入参起始位置
* @param
* @return (当前页-1)* 每页条数
*/
public int getFrom() {
return (current - 1) * size;
}
/**
* @description redis分页入参结束标志
* @param
* @return 起始位置 + 每页条数 - 1
*/
public int getTo() {
int start = getFrom();
return start + size - 1;
}
/**
* @description 是否最后一页
* @param
* @return 当前页 >= 总页数 ? true : false
*/
public boolean isEnd() {
return current >= totalSize;
}
/**
* @description 获取总页数
* @param
* @return
*/
public long getTotalSize() {
return totalSize;
}
public void setTotalSize() {
this.totalSize = (total + size - 1) / size;
}
}
上述代码是用于redis的分页数据包装的,业务数据最终包在data参数中。
public void collect(CollectVo collectVo) {
String collectKey = RedisKeyUtil.getCollectKey(collectVo);
String userKey = RedisKeyUtil.getUserKey(collectVo);
String userValueKey = RedisKeyUtil.getUserValueKey(collectVo);
redisTemplate.execute(new SessionCallback<Object>() {
@Override
public Object execute(@NonNull RedisOperations redis) throws DataAccessException {
Long member = redisTemplate.opsForZSet().rank(collectKey, collectVo.getUserId());
redis.multi();
if (null != member) {
redisTemplate.opsForZSet().remove(collectKey, collectVo.getUserId());
redisTemplate.opsForZSet().remove(userKey, collectVo.getCollectId());
redisTemplate.opsForHash().delete(userValueKey, collectVo.getCollectId());
} else {
redisTemplate.opsForZSet().add(collectKey, collectVo.getUserId(), System.currentTimeMillis());
redisTemplate.opsForZSet().add(userKey, collectVo.getCollectId(), System.currentTimeMillis());
redisTemplate.opsForHash().put(userValueKey, collectVo.getCollectId(), collectVo);
}
return redis.exec();
}
});
}
上述代码babybus:首先生成三个key,如下表格所示,先开启一个事务,使所有操作一块执行,然后rank操作首先对collectKey对应的userId的key进行排序,rank操作如果这个key不存在的话返回是空的,所以可以利用这个特点对数据进行判断,如果用户收藏了此对象,则不为空,就进行移除操作,也就是取消收藏,反之添加,在进行存储zset的索引列表时使用时间戳赋予分值进行排序,这样收藏的列表即为有序的了
需要注意:此处的事务并不是真正意义上的事务,只是为了同时执行一批操作,防止多次存取,因为redis不支持事务
redis key | 含义 | 格式 | 类型 |
---|---|---|---|
collectKey | 被收藏的对象key,保存value为用户id (便于统计该对象被多少人收藏,被哪些人收藏) | collect::1:id(key前缀+收藏类型+id) | zset |
userKey | 用户收藏了哪些对象key,保存value为收藏对象id (作用同上,用于索引数据) | user_collect::id(key前缀+用户id) | zset |
userValueKey | 用户收藏的索引对应的数据值(相应的业务实体) | user_collect_value::id(key前缀+用户id) | hash |
上述表格展示了三个redis key,全部的业务数据存储就使用到了这三个key,可以理解为数据库的二级索引,userKey只保存具体收藏对象的id并不存储实际数据,实际数据由hash类型的userValueKey来存储,可以看到该key由用户id进行区分,具体保存的键为收藏对象id,值为具体收藏对象业务实体。
这样设计的主要原因就是分离开之后可以对业务操作进行更多的更方便的查询,也是为什么没有像一级索引一样直接保存了业务数据而单独抽出来了一个索引key
/**
* @param collectVo .
* @return 查询收藏的数量
**/
public Long findCollectCount(CollectVo collectVo) {
String collectKey = RedisKeyUtil.getCollectKey(collectVo);
//直接size一下就获取到了
Long size = redisTemplate.opsForZSet().size(collectKey);
return Objects.nonNull(size) ? size : 0L;
}
/**
* @param collectVo .
* @return 查询当前收藏状态
**/
public Boolean findCollectStatus(CollectVo collectVo) {
String collectKey = RedisKeyUtil.getCollectKey(collectVo);
//rank一下如果收藏对象key里有该用户id则收藏
Long rank = redisTemplate.opsForZSet().rank(collectKey, collectVo.getUserId());
return rank != null;
}
入参vo:
public class MyCollectVo {
@NotNull
//当前页
private Integer current;
@NotNull
//每页条数
private Integer size;
@Override
public String toString() {
return "MyCollectVo{" +
"current=" + current +
", size=" + size +
'}';
}
public Integer getCurrent() {
return current;
}
public void setCurrent(Integer current) {
this.current = current;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
}
//这两行不需要特别关注,就是将入参转为上述的redis分页包装类page,然后collect参数里面主要用到一个userId,进行第二行业务查询
RedisPage<List<CollectArticleVo>> page = convertApiUtil.getPage(collectVo);
RedisPage<List<CollectArticleVo>> collectArticle = collectionService.getCollectArticle(page, collect);
真正的getCollectArticle业务处理如下
/**
* @return 分页查询收藏文章
**/
public RedisPage<List<CollectArticleVo>> getCollectArticle(RedisPage<List<CollectArticleVo>> page, CollectVo collectVo) {
//上述的用户收藏了哪些对象id的key 也就是用户收藏索引key
String userKey = RedisKeyUtil.getUserKey(collectVo);
//用户收藏的索引值key
String userValueKey = RedisKeyUtil.getUserValueKey(collectVo);
//索引列表 倒序 reverseRange操作分页获取倒序的索引列表
Set<Object> articleKey = redisTemplate.opsForZSet().reverseRange(userKey, page.getFrom(), page.getTo());
//查询收藏总数
Long size = redisTemplate.opsForZSet().size(userKey);
if (Objects.isNull(articleKey) || Objects.isNull(size)) {
throw new CommonException("查询size或索引列表为空!");
}
//设置数据总量
page.setTotal(size);
//设置总页数
page.setTotalSize();
//设置是否最后一页
page.isEnd();
//判空
if (page.getCurrent() > page.getTotalSize() || articleKey.isEmpty()) {
page.setData(new ArrayList<>());
return page;
}
//接下来用到了多线程操作,因为多条数据需要多次从索引值key中获取,所以进行了异步操作,可以去掉
//此处用map把set索引列表按序附上一个自增的score便于排序
//因为通过reverseRange查出来的set是有序的,但下方异步操作查询之后结果是乱序的需要重排序
ConcurrentHashMap<Object, Integer> keyMap = new ConcurrentHashMap<>(20);
int score = 0;
for (Object a : articleKey) {
keyMap.put(a, score++);
}
//声明个计数器,异步操作时执行完一批后返回
final CountDownLatch countDownLatch = new CountDownLatch(articleKey.size());
//线程安全列表包一下,操作的时候不会出问题
List<CollectArticleVo> countList = Collections.synchronizedList(new ArrayList<>());
//索引值
for (Map.Entry<Object, Integer> index : keyMap.entrySet()) {
MyThread.getExecutor().submit(() -> {
try {
//for循环从索引值列表里通过索引列表的id取
CollectVo artValue = (CollectVo) redisTemplate.opsForHash().get(userValueKey, index.getKey());
//获取索引值分数(时间戳,收藏时间)
Double timestamp = redisTemplate.opsForZSet().score(userKey, index.getKey());
CollectArticleVo collectArticleVo = new CollectArticleVo();
//此处为时间格式化,并不一定要有
if (timestamp != null) {
LocalDateTime dateTime = LocalDateTime.ofEpochSecond(timestamp.longValue() / 1000L, 0, ZoneOffset.ofHours(8));
String format = dateTime.format(DateTimeFormatter.ofPattern(PATTERN));
collectArticleVo.setCollectTime(format);
}
collectArticleVo.setId(index.getKey().toString().substring(index.getKey().toString().lastIndexOf(SPLIT) + 1));
collectArticleVo.setIntroduce(Objects.requireNonNull(artValue).getCollectIntroduce());
collectArticleVo.setSort(index.getValue());
countList.add(collectArticleVo);
} catch (Exception exception) {
log.error("查询我的收藏出错了: ", exception);
} finally {
countDownLatch.countDown();
}
});
}
try {
countDownLatch.await();
} catch (InterruptedException exception) {
log.error("查询我的收藏出错了: ", exception);
}
//最后进行重排序
Collections.sort(countList);
page.setData(countList);
return page;
}
上述代码里有很多的注释解释,就不一一说明了,到此完结。