为什么要使用redis?
如果使用mysql
要实现排行榜功能,如果使用 mysql或其它关系型数据库 来进行排行,我们大致的思路就是在这个表再定义出一个字段 如hot 来记录热度值,然后在数据库查询时通过 order by hot 来实现排行。如果有影响排行的操作就直接 update 修改数据库表。
这样做的优缺点也很明显,就拿博客排行而言,可能很多操作都会影响到热度的变化。如浏览、评论、点赞等等。
优点就是代码操作简单,有事项就执行sql语句修改一下热度值。
缺点就是频繁的数据库操作会对服务器造成一定的压力,效率也比较低。
redis实现
Redis中一共有 5 种数据类型,其中 ZSet 是一个比较特殊的数据类型,
我们可以先了解 zset 的大体结构,
zset 数据的value有多列,每列的成员包含 member 和 score
对于实现排行榜来说,一般member存放对应排行内容单项的 id 值,有相关的操作修改 score
score(分值)可以 来对存入数据进行一个排序。并通过
zincrby key increment member
来实现一个热度的原子递增。
那么使用 redis 来实现这个功能的优势已经不言而喻了,感觉 zset 这个数据类型就是为排行而生的,同时 redis 作为一项高性能 nosql 也更高地提高了程序的并发性能,效率也高很多。
redis为什么快,却又不能替代mysql?
这个问题我举一个例子让大家简单理解下这个问题。
类似于我们躺在房间里要拿东西(数据) ,如果东西就在房间里(服务器内存里),我们只要一伸手就能拿到。如果东西放在客厅或者储物间里(硬盘中),我们则需要走到外面去(效率低),才能拿到这项东西。但是房间里的空间有限(内存小),不能存放太多的物品。所以在东西多的时候,还是需要把大部分东西放在客厅或者储物间(mysql)。同时如果房间里东西多了(内存较满),也会影响我们的通行(影响执行效率)。
日榜、周榜实现方案介绍
既然是排行榜就要有区分排行榜每一天或每一个周的根据,我们通过操作 key 的后缀来实现
redis key生成策略
通过获取时间戳,除以每日的单位 1000 * 60 * 60 * 24 得到一个 day key ,积累今日用户操作进行的分值累积
通过算法算出上周的 week key,对上周 day key 进行累积合并操作生成
得到的 day key 或 week key 将用来作为排行榜存入redis的 key 后置参数
(假设daykey 为 5,那么就生成 key 为 rank_day:5),redis可视化工具中:将为我们自动分文件夹
创建key时,指定 TTL 为 40天,避免垃圾数据占用内存
通过合并操作,来实现周、月排行榜
热度增加代码参考
热度增加,在当日的 key上进行操作,如果接口请求日榜就返回当日信息,如果请求的是周榜就新型合并操作
这里进行是否创建判断主要是为了 设置 TTL,避免造成空间浪费
@Override
public void addRankHotScore(Integer blogId, Double score) {
// 获取dayKey
long dayKey = RankKeyUtils.getDayKey();
//封装 redis key
String key = RANK_HOT_DAY_KEY + dayKey;
//进行判断,是否已经创建 该redis
if (Boolean.FALSE.equals(redisTemplate.hasKey(key))) {
//如果没有创建,就执行创建并设置 TTL 为40天
redisTemplate.opsForZSet().incrementScore(key, blogId, score);
redisTemplate.expire(key, RANK_HOT_DAY_TTL, TimeUnit.SECONDS);
}
//已创建,将对应博客热度增加 3
redisTemplate.opsForZSet().incrementScore(key, blogId, score);
}
日榜获取
日 key算法:
/**
* 获取 day key
*/
public static long getDayKey() {
return System.currentTimeMillis() / (1000 * 60 * 60 * 24);
}
获取日榜信息
@Override
public List<RankHotVO> getTodayHotRank() {
// 1 进行判断 redis 是否已经创建了 今天的redis 排行榜缓存的key
// 1.1 获取 day key
long dayKey = RankKeyUtils.getDayKey();
// 获取redis数据
Set<ZSetOperations.TypedTuple<Integer>> typedTuples = redisTemplate.opsForZSet().reverseRangeWithScores(RANK_HOT_DAY_KEY + dayKey, 0, -1);
return getRankHotVOList(typedTuples);
}
周榜获取
周 key算法
/**
* 获取上一周的 week key
*/
public static long getWeekKey() {
// 拿到 week key,和day key
long week = System.currentTimeMillis() / (1000 * 60 * 60 * 24 * 7);
long day = getDayKey();
// 时间戳取模到周key,不是刚好从周一算起的,如果dayKey%7==4,这样这天才是刚好周一
// 由于原来的week key并不是从周一开始算起,所以进行移位计算,拿到上周的week key
long key = day % 7;
if (key >= 4) {
// 经计算,取模值大于等于 4 时 进入新的一周,将week key-1
return week - 1;
}
//否则,在新的一周的周四时,week值将再加1,所以要拿到上一周的week key需要-2
return week - 2;
}
获取周榜信息
@Override
public List<RankHotVO> getWeekHotRank() {
long weekKey = RankKeyUtils.getWeekKey();
// 拿到数据集合
Set<ZSetOperations.TypedTuple<Integer>> weekRank = redisTemplate.opsForZSet().reverseRangeWithScores(RANK_HOT_WEEK_KEY + weekKey, 0, -1);
// 进行判断查看有没有数据
if (!(weekRank == null || weekRank.size() == 0)) {
// 如果拿到数据直接进行查询并返回
return getRankHotVOList(weekRank);
}
// 如果没有拿到数据集,进行合并操作并存入缓存
// 获取 day key
long dayKey = RankKeyUtils.getDayKey();
// 计算需要进行聚合的上周一的 day key
long mondayKey = dayKey % 7;
if (mondayKey >= 4) {
mondayKey = dayKey - (3 + mondayKey);
} else {
mondayKey = dayKey - (10 + mondayKey);
}
// 聚合上周的day key,存入集合
ArrayList<String> dayKeys = new ArrayList<>();
for (long i = mondayKey; i < mondayKey + 7; i++) {
dayKeys.add(RANK_HOT_DAY_KEY + i);
}
// 合并操作获取合并结果
redisTemplate.opsForZSet().unionAndStore("COUNT_WEEK", dayKeys, RANK_HOT_WEEK_KEY + weekKey);
// 设置过期时间为 一星期
redisTemplate.expire(RANK_HOT_WEEK_KEY + weekKey, RANK_HOT_WEEK_TTL, TimeUnit.SECONDS);
weekRank = redisTemplate.opsForZSet().reverseRangeWithScores(RANK_HOT_WEEK_KEY + weekKey, 0, -1);
return getRankHotVOList(weekRank);
}
这其实有和日榜差不多,主要区别就是需要统计几天的数据进行合并操作
上述 return中方法主要就是根据 id 进行数据项查找
/**
* 拿到博客id及热度后,设置 热度排行榜显示 的相关参数
*
* @param typedTuples 排行数据
* @return 热点排行数据列表
*/
private List<RankHotVO> getRankHotVOList(Set<ZSetOperations.TypedTuple<Integer>> typedTuples) {
if (typedTuples == null || typedTuples.size() == 0) {
// 如果没有创建,说明今天暂无热榜相关的信息,返回空信息
return null;
}
// 2 如果创建了,封装响应信息
//拿到set集合迭代器
Iterator<ZSetOperations.TypedTuple<Integer>> iterator = typedTuples.iterator();
List<RankHotVO> result = new ArrayList<>();
// 创建博客id集合
List<Integer> blogIdList = new ArrayList<>();
while (iterator.hasNext()) {
//拿到这项信息
ZSetOperations.TypedTuple<Integer> tuple = iterator.next();
// 拿到 blogId
Integer blogId = tuple.getValue();
// 2.1 将博客id存入集合
blogIdList.add(blogId);
// 2.2 设置热度信息
RankHotVO rankHotVO = new RankHotVO();
rankHotVO.setHot(tuple.getScore());
result.add(rankHotVO);
}
List<Blog> blogList = blogMapper.selectBatchIds(blogIdList);
// 创建 用户id集合
List<Integer> userIdList = new ArrayList<>();
for (Blog blog : blogList) {
// 拿出 userId 存入集合
userIdList.add(blog.getAuthorId());
}
// 查询并设置 user信息
Map<Integer, UserDTO> userList = userClient.getUserList(userIdList).getData();
for (int i = 0; i < blogList.size(); i++) {
// 设置用户相关信息
result.get(i).setBlog(blogList.get(i));
result.get(i).setAuthor(userList.get(blogList.get(i).getAuthorId()));
}
return result;
}
为了提高效率,可以先对需要查找的 id 进行统计,然后一次查找,减少数据库压力。
最后可以看看我的开源项目: i集大校园(类似于一个定位为校园里的微博)
i集大校园软件服务端,基于SpringCloud Alibaba 微服务组件及部分分布式技术实现服务之间关联及协作进行前后端分离项目实现。计划实现微信小程序和app两端同步。
使用技术栈为:Spring Boot、Spring Cloud Alibaba、rabbitMQ、JWT、minIO、mysql、redis、ES、docker、Jenkins、mybatis-plus
前端使用 微信小程序编写。
欢迎一起参加开源贡献和star项目哈!