阅读量:0
效果展示
好处
首先初始化所有浏览量
访问文章后增加的浏览量**不直接修改数据库
,先存到redis然后访问也是获取redis的浏览量,做个定时任务,后续自定义时间同步数据库
**,好像也就是一个集中处理罢了
CommandLineRunner实现项目启动时预处理
新建包
新建runner初始化包和新建viewsRunner初始化浏览类
package com.example.vueelementson.runner; import com.example.vueelementson.dao.ArticleDao; import com.example.vueelementson.entity.Article; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * @BelongsProject: blog-springboot * @BelongsPackage: com.example.vueelementson.runner * @Author: Zww * @CreateTime: 2024-08-02 11:53 * @Description: TODO * @Version: 1.0 */ @Component public class ViewsRunner implements CommandLineRunner { @Autowired private ArticleDao articleDao; @Override public void run(String... args) throws Exception { // 查询每一篇博客 id对应浏览量,封装map-> articleId,views List<Article> articles = articleDao.selectList(null); Map<Integer, Integer> viewsMap = articles.stream().collect(Collectors.toMap(Article::getId, article -> article.getViews())); } }
RedisTemplate工具类
package com.example.vueelementson.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.BoundSetOperations; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import java.util.*; import java.util.concurrent.TimeUnit; @Component public class RedisCache { @Autowired public RedisTemplate redisTemplate; // 设置map属性值的自增(浏览量递增) public void incrementMapValueCache(String key, String hkey, int step) { redisTemplate.opsForHash().increment(key, hkey, step); } /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 */ public <T> void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @param unit 时间单位 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 获取有效时间 * * @param key Redis键 * @return 有效时间 */ public long getExpire(final String key) { return redisTemplate.getExpire(key); } /** * 判断 key是否存在 * * @param key 键 * @return true 存在 false不存在 */ public Boolean hasKey(String key) { return redisTemplate.hasKey(key); } /** * 获得缓存的基本对象。 * * @param key 缓存键值 * @return 缓存键值对应的数据 */ public <T> T getCacheObject(final String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 删除单个对象 * * @param key */ public boolean deleteObject(final String key) { return redisTemplate.delete(key); } /** * 删除集合对象 * * @param collection 多个对象 * @return */ public boolean deleteObject(final Collection collection) { return redisTemplate.delete(collection) > 0; } /** * 缓存List数据 * * @param key 缓存的键值 * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public <T> long setCacheList(final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 获得缓存的list对象 * * @param key 缓存的键值 * @return 缓存键值对应的数据 */ public <T> List<T> getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 缓存Set * * @param key 缓存键值 * @param dataSet 缓存的数据 * @return 缓存数据的对象 */ public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) { BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key); Iterator<T> it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 获得缓存的set * * @param key * @return */ public <T> Set<T> getCacheSet(final String key) { return redisTemplate.opsForSet().members(key); } /** * 缓存Map * * @param key * @param dataMap */ public <T> void setCacheMap(final String key, final Map<String, T> dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 获得缓存的Map * * @param key * @return */ public <T> Map<String, T> getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入数据 * * @param key Redis键 * @param hKey Hash键 * @param value 值 */ public <T> void setCacheMapValue(final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } /** * 获取Hash中的数据 * * @param key Redis键 * @param hKey Hash键 * @return Hash中的对象 */ public <T> T getCacheMapValue(final String key, final String hKey) { HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } /** * 获取多个Hash中的数据 * * @param key Redis键 * @param hKeys Hash键集合 * @return Hash对象集合 */ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 删除Hash中的某条数据 * * @param key Redis键 * @param hKey Hash键 * @return 是否成功 */ public boolean deleteCacheMapValue(final String key, final String hKey) { return redisTemplate.opsForHash().delete(key, hKey) > 0; } /** * 获得缓存的基本对象列表 * * @param pattern 字符串前缀 * @return 对象列表 */ public Collection<String> keys(final String pattern) { return redisTemplate.keys(pattern); } }
注入工具类并使用
@Autowired private ArticleDao articleDao; @Autowired private RedisCache redisCache; @Override public void run(String... args) throws Exception { // 查询每一篇博客 id对应浏览量,封装map-> articleId,views List<Article> articles = articleDao.selectList(null); Map<String , Integer> viewsMap = articles.stream().collect(Collectors.toMap(article -> article.getId().toString(), article -> article.getViews())); redisCache.setCacheMap("article:views", viewsMap); }
Redis序列化乱码配置类
package com.example.vueelementson.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { @Bean public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory connectionFactory){ RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); //设置String类型的key设置序列化器,方便在可视化redis控制器里查看 redisTemplate.setKeySerializer(new StringRedisSerializer()); // String类型 value序列器 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); //设置Hash类型的key设置序列化器,方便在可视化redis控制器里查看 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // Hash类型 value序列器 redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); //设置redis链接工厂 redisTemplate.setConnectionFactory(connectionFactory); return redisTemplate; } }
根据id查询文章时增加浏览量
也可以单独封装到service或方法里,或封装个接口前端多发一次请求,实现解耦
@GetMapping(value = "/{id}") public Result selectOne(@PathVariable("id") Long id, HttpServletRequest request, HttpServletResponse response) { Article byId = articleService.getById(id); redisCache.incrementMapValueCache("article:views", id.toString(), 1); return Result.success(byId); }
自行访问并且查看redis值
定时任务更新数据库
设置定时任务,每隔5秒更新一次,或者自定义时间
启动类加注解@EnableScheduling
package com.example.vueelementson; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @MapperScan("com.example.vueelementson.dao") @EnableAsync @EnableScheduling public class VueElementSonApplication { public static void main(String[] args) { SpringApplication.run(VueElementSonApplication.class, args); } }
定时任务类
//从0秒开始过5秒执行一次,不写的话会变成每分钟的第五秒执行 @Scheduled(cron = "0/5 * * * *?") public void updateViewCount() { Map<String, Integer> cacheMap = redisCache.getCacheMap("article:views"); // 获取map所有的entry集合转换为article对象,有id和浏览量属性,然后通过mp的iservice接口的批量更新实体方法将所有缓存里的浏览量更新到数据库 List<Article> articleList = cacheMap.entrySet() .stream() .map(entry -> new Article(Integer.parseInt(entry.getKey()), entry.getValue())) .collect(Collectors.toList()); // 调用mp方法更新 articleService.updateBatchById(articleList); System.out.println("更新完毕"); }
自定义时间例子参考
每天上午 9 点执行任务: java 复制 @Scheduled(cron = "0 0 9 * * ?") public void runTaskAtNineAM() { // 在每天上午 9 点执行的任务逻辑 } Cron 表达式 "0 0 9 * * ?" 的含义是: 秒: 0 秒 (光写秒意思为每分钟的第几秒,前面带个0/意思为从0开始每隔五秒执行一次) 分: 0 分 时: 9 点 日: 不关心 月: 不关心 周: 不关心 每个工作日下午 5 点执行任务: java 复制 @Scheduled(cron = "0 0 17 ? * MON-FRI") public void runTaskAtFivePM() { // 在每个工作日下午 5 点执行的任务逻辑 } Cron 表达式 "0 0 17 ? * MON-FRI" 的含义是: 秒: 0 秒 分: 0 分 时: 17 点 (下午 5 点) 日: 不关心 月: 不关心 周: 周一到周五 (MON-FRI) 每月 1 号和 15 号的 23 点 30 分执行任务: java 复制 @Scheduled(cron = "0 30 23 1,15 * ?") public void runTaskAtTheFirstAndFifteenth() { // 在每月 1 号和 15 号的 23 点 30 分执行的任务逻辑 } Cron 表达式 "0 30 23 1,15 * ?" 的含义是: 秒: 0 秒 分: 30 分 时: 23 点 (11 点) 日: 1 号和 15 号 月: 不关心 周: 不关心
如果你懒得计算了,直接使用cron表达式生成器,和设闹钟一个道理
Cron - 在线Cron表达式生成器 (ciding.cc)
cron语法
六个* 秒分时日月周
?表示该时间单位不计入,只可单独用在具体几号和星期几,同时使用发生冲突
*表示任意值
1,2,3表示时间列表,表示1,2,3秒都执行
1-5 一到五秒内的时间点执行
0/5 从0秒开始每隔5秒 0/*可以写成/*
周1-7 1为星期日
0 0 0 31W * ? W表示最接近几号的工作日,31号是星期六,那么就为30,星期日则为29
L表示每个月的最后一天last
LW表示每月最后一个工作日
6L表示每月最后一个星期五
0 0 0 ? * 6#3表示第三个星期五
获取文章数据时从redis拿浏览量
此时我们还可以引入cookie判断用户是否携带已浏览的 cookie键值,如果不存在则增加redis值,然后标记冷却时间也就是给响应体赋予cookie存活时间自定义,如果存在,则不增加浏览量,等到cookie死掉才进行增加,可以防止被多线程循环发请求爆破
@GetMapping(value = "/{id}") public Result selectOne(@PathVariable("id") Integer id, HttpServletRequest request, HttpServletResponse response) { Article byId = articleService.getById(id); if (StrUtil.isEmpty(CookieUtils.getCookieValue(request, Article.VIEW_COOKIES + id))) { redisCache.incrementMapValueCache("article:views", id.toString(), 1); CookieUtils.setCookie(request, response, Article.VIEW_COOKIES + id, "viewd", 60 * 60); System.out.println("浏览量增加冷却时间已过,开始增加"); } Integer redisViews = redisCache.getCacheMapValue("article:views", id.toString()); byId.setViews(redisViews); return Result.success(byId); // 如果cookie为空,说明第一次访问,访问量可以增加,隔一小时再看浏览量才能增加 }