⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/m0_46937429/article/details/119172118 「我的代码没错」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

思路分析

做每周热议,应该用缓存来做,如果直接查库的话,会对数据库造成压力。用缓存做的话,用Redis 来做缓存的话比较合适一点。

## 利用Redsi 添加 数据命令
## day:1 指的是在1号的时候 post:1 第一篇文章添加了 10 条评论。
#后面 6 post:2 指的是 在1号第二篇添加了6条评论
zadd day:1 10 post:1 6 post:2
zadd day:2 10 post:1 6 post:2
zadd day:3 10 post:1 6 post:2
....
zadd day:8 10 post:1 6 post:2
#这样就完成了7天的记录

上面的命令可以帮我们记录一下7天的所有的评论数。但是还没有帮我们计算出来谁是评论最高的。看Redis 的sorted set有序集合有个命令就可以帮我们实现这个功能。

这个命令可以帮助我们实现并集,我们只需要把7天的评论给做个并集就可以求出来。

## Redis 命令
#意思是并集把这7天的 放到一个新的集合里面 新的集合是 week:rank 这样这个新的集合里面就有了我们的
#7天的记录了
union week:rank 7 day:1...day:8

Redis 命令实践一下看看

本地命令行测试

127.0.0.1:6379> ping
PONG
127.0.0.1:6379> zadd day:1 10 post:1
(integer) 1
127.0.0.1:6379> zadd day:2 10 post:1
(integer) 1
127.0.0.1:6379> zadd day:3 10 post:1
(integer) 1
127.0.0.1:6379> zadd day:1 5 post:2
(integer) 1
127.0.0.1:6379> zadd day:2 5 post:2
(integer) 1
127.0.0.1:6379> zadd day:3 5 post:2
(integer) 1
127.0.0.1:6379> keys *
1) "day:1"
2) "day:2"
3) "day:3"

查看当天的排行榜命令 ZRevrange

127.0.0.1:6379> zrevrange day:1 0 -1 withscores
1) "post:1"
2) "10"
3) "post:2"
4) "5"
127.0.0.1:6379>

每周的评论排行榜记录。因为我只有三天的,所以只写了3天的

127.0.0.1:6379> zrevrange week:rank 0 -1 withscores
1) "post:1"
2) "30"
3) "post:2"
4) "15"
127.0.0.1:6379>

上面的记录是没有错误的。上述的命令可以帮我们简单的实现了我们的想法,下面用代码来实现。上面还有一个小的 bug 就是当day:1这一天可能会出现就是不可能直接就过完了。可能会一条一条的增加,这个时候应该使用的是自增这个命令来解决这个问题。

#什么时候+1 什么时候-1 就是当你 添加一条评论的时候就添加1 删除的的时候就减1
ZINCRBY day:1 10 post:1

代码来进行实现

目前前端的样式,这样的话我们就需要在项目一开始的时候就启动这个功能

@Component
// 实现 启动类 ,还有 上下文的servlect
public class ContextStartup implements ApplicationRunner, ServletContextAware {
// 注入 categoryService
@Autowired
IMCategoryService categoryService;
ServletContext servletContext;
// 注入post 的服务类
@Autowired
IMPostService postService;

@Override
public void run(ApplicationArguments args) throws Exception {
// 调用全查的方法
List<MCategory> list = categoryService.list(new QueryWrapper<MCategory>().eq("status", 0));
servletContext.setAttribute("List", list);
// 调用每周热评的方法
postService.initweek();
}

@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
}

服务类serviceimpl类

大概的思路

  • 获取7天内发表的文章

  • 初始化文章的总阅读量

    • 缓存文章的基本信息(id,标题,评论数,作者 ID )

    • 这样就可以避免的查库。可以直接用我们的缓存。

  • 做并集

这里需要一个Redis 的工具类,我在网上找到的,不是我写的。网上一大堆。直接拿来用就可以了。

package com.example.springbootblog.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

@Autowired
private RedisTemplate redisTemplate;

/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}

/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}

//============================String=============================

/**
* 普通缓存获取
*
* @param key 键
* @return
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}

/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}

}

/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}

/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}

//================================Map=================================

/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}

/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}

/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}

/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}

/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}

/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}

//============================set=============================

/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}

/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}

/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}

/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================

/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}

/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}

//================有序集合 sort set===================
/**
* 有序set添加元素
*
* @param key
* @param value
* @param score
* @return
*/
public boolean zSet(String key, Object value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}

public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) {
return redisTemplate.opsForZSet().add(key, typles);
}

public void zIncrementScore(String key, Object value, long delta) {
redisTemplate.opsForZSet().incrementScore(key, value, delta);
}

public void zUnionAndStore(String key, Collection otherKeys, String destKey) {
redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);
}

/**
* 获取zset数量
* @param key
* @param value
* @return
*/
public long getZsetScore(String key, Object value) {
Double score = redisTemplate.opsForZSet().score(key, value);
if(score==null){
return 0;
}else{
return score.longValue();
}
}

/**
* 获取有序集 key 中成员 member 的排名 。
* 其中有序集成员按 score 值递减 (从大到小) 排序。
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
}

}

实现类的代码

// 每周热评的方法
@Override
public void initweek() {
//获取 7天的文章
List<MPost> posts = this.list(new QueryWrapper<MPost>().ge("created", DateUtil.lastWeek())
.select("id", "title", "user_id", "comment_count", "view_count", "created")
);// 获取到7天前的以及按照这几个查询,不需要全部查询
// 初始化文章的总评论
for (MPost post : posts) {
// 设置 key
String key = "day:rank:" + DateUtil.format(post.getCreated(), DatePattern.PURE_DATE_FORMAT);
// 缓存进去的评论数量
redisUtil.zSet(key, post.getId(), post.getCommentCount());
//设置自动过期 7天过期
long between = DateUtil.between(new Date(), post.getCreated(), DateUnit.DAY);
long expireTime = (7 - between) * 24 * 60 * 60; // 有效 时间

redisUtil.expire(key, expireTime);
// 缓存文章的一些基本信息
this.hashCachePost(post, expireTime);
}
// 做并集
this.zunionAndStore();
}

/**
* 文章每周评论数量并集操作
**/
private void zunionAndStore() {
String destkey = "day:rank:" + DateUtil.format(new Date(), DatePattern.PURE_DATE_FORMAT);
// 设置并集后的新的 key
String newkey = "week:rank";
ArrayList<String> otherKeys = new ArrayList<>();
// 计算7天的
for (int i = -6; i < 0; i++) {
String temp = "day:rank:" + DateUtil.format(DateUtil.offsetDay(new Date(), i), DatePattern.PURE_DATE_FORMAT);
otherKeys.add(temp);
}
redisUtil.zUnionAndStore(destkey, otherKeys, newkey);
}

/**
* 文章作者缓存
**/
private void hashCachePost(MPost post, long expireTime) {
// 设置 key
String key = "rank:post:" + post.getId();
// 判断存在不存在
boolean hasKey = redisUtil.hasKey(key);
if (!hasKey) {
// 就存到缓存里面
redisUtil.hset(key, "post:id", post.getId(), expireTime);
redisUtil.hset(key, "post:title", post.getTitle(), expireTime);
redisUtil.hset(key, "post:commentCount", post.getCommentCount(), expireTime);

}
}
}

这样就可以把我们的命令行转换成代码的形式。就可以把我们的数据库的数据先存到缓存中去了。

效果

127.0.0.1:6379> keys *
1) "rank:post:4"
2) "week:rank"
3) "day:rank:20210724"
4) "rank:post:3"
5) "rank:post:2"
6) "day:rank:20210726"
#查看我们并集完后的数据 id 为 3 的有 1条评论。
127.0.0.1:6379> zrevrange week:rank 0 -1 withscores
1) "3"
2) "1"
3) "2"
4) "1"
5) "4"
6) "0"
127.0.0.1:6379>

数据库中id 为 3 的有 1条评论

确实只有一条评论的。

前端展示出来

这里的思路就比较简单了,把我们的数据从缓存中取出来就可以。用的freemarker可以自定义标签。我自定义了标签。

Hosttemplate

/**
* 本周热议
*/
@Component
public class HotsTemplate extends TemplateDirective {

@Autowired
RedisUtil redisUtil;

@Override
public String getName() {
return "hots";
}

@Override
public void execute(DirectiveHandler handler) throws Exception {
// 设置 key
String key ="week:rank";
Set<ZSetOperations.TypedTuple> zSetRank = redisUtil.getZSetRank(key, 0, 6);
ArrayList<Map> maps = new ArrayList<>();

// 便利
for (ZSetOperations.TypedTuple typedTuple : zSetRank) {
// 创建 Map
HashMap<String, Object> map = new HashMap<>();
Object post_id = typedTuple.getValue();
String PostHashKey = "rank:post:" +post_id;
map.put("id",post_id);
map.put("title",redisUtil.hget(PostHashKey,"post:title"));
map.put("commentCount",typedTuple.getScore());
maps.add(map);
}
handler.put(RESULTS,maps).render();
}
}

FreemarkerConfig把我们写的便签注入就可以使用我们自定义的标签了

@Configuration
public class FreemarkerConfig {

@Autowired
private freemarker.template.Configuration configuration;
@Autowired
PostsTemplate postsTemplate;
@Autowired
HotsTemplate hotsTemplate;
@PostConstruct
public void setUp() {
configuration.setSharedVariable("timeAgo", new TimeAgoMethod());
configuration.setSharedVariable("posts", postsTemplate);
configuration.setSharedVariable("hosts", hotsTemplate);
}


}

前端的页面获取

效果

总结

在做这个功能的时候。不够全面。虽然写完了,但是应该是获取7天内的评论。我获取了7天的文章。虽然是个 bug 但是我不想修复。就这样吧。现在能用就行。道理是一样的。到时候有时间出问题了在改吧。累了,无情的代码机器记录代码生活中。。。。。

文章目录
  1. 1. 思路分析
    1. 1.1. Redis 命令实践一下看看
    2. 1.2. 代码来进行实现
      1. 1.2.1. 服务类serviceimpl类
    3. 1.3. 前端展示出来
  2. 2. 总结