共计 14338 个字符,预计需要花费 36 分钟才能阅读完成。
思路剖析
做每周热议,应该用缓存来做,如果间接查库的话,会对数据库造成压力。用缓存做的话,用 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 的工具类,我在网上找到的,不是我写的。网上一大堆。间接拿来用就能够了。
举荐一个开源收费的 Spring Boot 最全教程:
https://github.com/javastacks/spring-boot-best-practice
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 然而我不想修复。就这样吧。当初能用就行。情理是一样的。到时候有工夫出问题了在改吧。累了,有情的代码机器记录代码生存中。。。。。
起源:blog.csdn.net/m0_46937429/article/details/119172118
近期热文举荐:
1.1,000+ 道 Java 面试题及答案整顿(2022 最新版)
2. 劲爆!Java 协程要来了。。。
3.Spring Boot 2.x 教程,太全了!
4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!