像CSDN的点赞功能只记录了数量,微信朋友圈的点赞功能有显示点赞人头像(获取userId查询用户信息封装返回即可)
点赞、取消点赞是高频次的操作,若每次都读写数据库,大量的操作会影响数据库性能,甚至宕机,所以用缓存处理再合适不过。本文以文章点赞为例来展开叙述
数据格式选择 Redis有5种数据结构分别为:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)
。
由于需要记录文章和点赞人,还有点赞状态(点赞、取消),分析下 Redis 数据格式中Hash
最合适。
因为Hash
里的数据都是存在一个Key
中,通过Key
很方便的把所有的点赞数据都取出。Key
里面的数据还可以存成键值对
的形式,方便存入点赞人、被点赞人和点赞状态
。
文章 id
为 articleId
,点赞人的 id
为 userId
,点赞状态
为 1(点赞)和0(取消点赞)
。文章 id
和点赞人 id
作为HashKey
,两个 id 中间用::
隔开,点赞状态
作为HashValue
。
整合SpringBoot 依赖 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
基础配置 spring: redis: database: 0 host: 127.0.0.1 port: 6379 jedis: pool: max-active: 8 max-wait: -1ms max-idle: 8 min-idle: 0
配置类 开启Redis事务支持
和序列化
package cn.goitman.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.transaction.PlatformTransactionManager;import javax.sql.DataSource;import java.sql.SQLException;@Configuration public class RedisConfig { @Bean public RedisTemplate redisTemplate (RedisConnectionFactory redisConnectionFactory) { RedisTemplate template = new RedisTemplate (); template.setKeySerializer(new StringRedisSerializer ()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer ()); template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer ()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer ()); template.setConnectionFactory(redisConnectionFactory); template.setEnableTransactionSupport(true ); return template; } @Bean public PlatformTransactionManager transactionManager (DataSource dataSource) throws SQLException { return new DataSourceTransactionManager (dataSource); } }
如果redisTemplate没有序列化
,在可视化工具中看到的数据为乱码,获取数据时也可能为空
,模糊查询(下文有叙述)功能也使用不了
Redis接口 package cn.goitman.service.impl;import cn.goitman.pojo.Article;import cn.goitman.pojo.Likes;import cn.goitman.service.RedisService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.Cursor;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.ScanOptions;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.stream.Collectors;@Service @Transactional public class RedisServiceImpl implements RedisService { public static final String KEY_ARTICLE_LIKE = "ARTICLE_LIKE" ; public static final String KEY_ARTICLE_LIKE_COUNT = "ARTICLE_LIKE_COUNT" ; @Autowired RedisTemplate redisTemplate; @Override public void saveLike (String articleId, String userId) { String field = getLikeKey(articleId, userId); redisTemplate.opsForHash().put(KEY_ARTICLE_LIKE, field, 1 ); redisTemplate.opsForHash().increment(KEY_ARTICLE_LIKE_COUNT, articleId, 1 ); } @Override public void unLike (String articleId, String userId) { String field = getLikeKey(articleId, userId); redisTemplate.opsForHash().put(KEY_ARTICLE_LIKE, field, 0 ); redisTemplate.opsForHash().increment(KEY_ARTICLE_LIKE_COUNT, articleId, -1 ); } @Override public void deleteLike (List<Likes> list) { for (Likes like : list) { String field = getLikeKey(like.getArticleId(), like.getUserId()); redisTemplate.opsForHash().delete(KEY_ARTICLE_LIKE, field); } } @Override public void deleteLikeCount (String articleId) { redisTemplate.opsForHash().delete(KEY_ARTICLE_LIKE_COUNT, articleId); } @Override public List<Likes> getAllLikeData () { List<Likes> list = new ArrayList <>(); Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(KEY_ARTICLE_LIKE, ScanOptions.NONE); while (cursor.hasNext()) { Map.Entry<Object, Object> entry = cursor.next(); String keys = entry.getKey().toString(); String[] keyArr = keys.split("::" ); Likes like = new Likes (keyArr[0 ], keyArr[1 ], (Integer) entry.getValue()); list.add(like); } return list; } @Override public List<Article> getArticleLikeCount () { List<Article> list = new ArrayList <>(); Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(KEY_ARTICLE_LIKE_COUNT, ScanOptions.NONE); while (cursor.hasNext()) { Map.Entry<Object, Object> entry = cursor.next(); String articleId = entry.getKey().toString(); Article article = new Article (articleId, (Integer) entry.getValue()); list.add(article); } return list; } private String getLikeKey (String articleId, String userId) { return new StringBuilder ().append(articleId).append("::" ).append(userId).toString(); } }
搞掂,就是这么简单高效,在Redis内,存在相同数据只会修改value,并且Redis默认RDB持久化数据
。
当然也可加上限时内限制每个用户点赞次数
的逻辑,防止恶意刷接口
,逻辑简单,在此就不累述啦
有人问:”点赞功能完全用Redis替代业务数据存储,该怎么查询指定数据呢?” 模糊查询参上
模糊查询 Redis是支持通配符模糊查询的(不用通配符就是精确查找啦
)
*
:通配任意多个字符?
:通配单个字符[]
:通配括号内的某一个字
查询Key
redisTemplate.keys(pattern)
@Override public List<String> fuzzyQueryKey (String key) { List<String> userIdList = (List<String>) redisTemplate.keys("*" + key + "*" ) .stream() .collect(Collectors.toList()); return userIdList; }
查询Hash数据中的HK
redisTemplate.opsForHash().scan(KEY,ScanOptions.scanOptions().match(pattern).build())
@Override public List<String> fuzzyQueryHashKey (String articleId) { List<String> userIdList = new ArrayList <>(); Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash() .scan(KEY_ARTICLE_LIKE, ScanOptions.scanOptions() .match("*" + articleId + "*" ) .build()); while (cursor.hasNext()) { Map.Entry<Object, Object> entry = cursor.next(); String[] keyArr = entry.getKey().toString().split("::" ); userIdList.add(keyArr[1 ]); } return userIdList; }
还是那句话,需要配置RedisTemplate的序列化,否则获取数据为空;
又有人说啦:”还是要固定间隔时间从Redis中捞取数据,保存在数据库中可靠点。” !@#$%^&* 业务说的都对,没办法,来吧
表设计 CREATE TABLE `article` ( `article_id` varchar (11 ) NOT NULL , `like_count` int (11 ) NOT NULL COMMENT '点赞数量' , `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间' , PRIMARY KEY (`article_id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8 COMMENT= '文章表' ;
CREATE TABLE `likes` ( `article_id` varchar (32 ) NOT NULL COMMENT '被点赞的文章id' , `user_id` varchar (32 ) NOT NULL COMMENT '点赞的用户id' , `status` tinyint(1 ) DEFAULT '1' COMMENT '点赞状态,0取消,1点赞' , `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间' , UNIQUE KEY `likeId` (`article_id`,`user_id`) USING BTREE ) ENGINE= InnoDB DEFAULT CHARSET= utf8 COMMENT= '文章点赞表' ;
POJO package cn.goitman.pojo;public class Article { private String articleId; private Integer LikeCount; public Article () { } public Article (String articleId, Integer likeCount) { this .articleId = articleId; LikeCount = likeCount; } public String getArticleId () { return articleId; } public void setArticleId (String articleId) { this .articleId = articleId; } public Integer getLikeCount () { return LikeCount; } public void setLikeCount (Integer likeCount) { LikeCount = likeCount; } }
package cn.goitman.pojo;public class Likes { private String articleId; private String userId; private Integer status; public Likes () { } public Likes (String articleId, String userId, Integer status) { this .articleId = articleId; this .userId = userId; this .status = status; } public String getArticleId () { return articleId; } public void setArticleId (String articleId) { this .articleId = articleId; } public String getUserId () { return userId; } public void setUserId (String userId) { this .userId = userId; } public Integer getStatus () { return status; } public void setStatus (Integer status) { this .status = status; } }
数据库操作 从Redis中获取数据保存到数据库后,删除Redis中相应数据
package cn.goitman.service.impl;import cn.goitman.mapper.LikeDao;import cn.goitman.pojo.Article;import cn.goitman.pojo.Likes;import cn.goitman.service.LikeService;import cn.goitman.service.RedisService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.List;@Service @Transactional public class LikeServiceImpl implements LikeService { @Autowired private RedisService redisService; @Autowired private LikeDao likeDao; @Override public void savaLikeData2DB () { List<Likes> likeList = redisService.getAllLikeData(); if (likeList.size() > 0 ) { for (Likes like : likeList) { Likes likes = likeDao.getLikesList(like); if (likes != null ) { likes.setStatus(like.getStatus()); likeDao.updataLike(likes); } else { likeDao.saveLike(like); } } redisService.deleteLike(likeList); } } @Override public void saveArticleLikeCount2DB () { List<Article> articleList = redisService.getArticleLikeCount(); if (articleList.size() > 0 ) { for (Article article : articleList) { Article articleData = likeDao.getArticleData(article.getArticleId()); if (articleData != null ) { articleData.setLikeCount(articleData.getLikeCount() + article.getLikeCount()); likeDao.updataArticle(articleData); } else { likeDao.saveArticle(article); } redisService.deleteLikeCount(article.getArticleId()); } } } }
package cn.goitman.mapper;import cn.goitman.pojo.Article;import cn.goitman.pojo.Likes;import org.apache.ibatis.annotations.Mapper;import java.util.List;@Mapper public interface LikeDao { Likes getLikesList (Likes likes) ; void saveLike (Likes likes) ; void updataLike (Likes likes) ; Article getArticleData (String articleId) ; void saveArticle (Article article) ; void updataArticle (Article article) ; }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="cn.goitman.mapper.LikeDao" > <select id ="getLikesList" resultType ="cn.goitman.pojo.Likes" > select article_id,user_id,status from likes where article_id = #{articleId} and user_id = #{userId} </select > <insert id ="saveLike" flushCache ="true" > insert into likes (article_id,user_id,status) values (#{articleId},#{userId},#{status}) </insert > <update id ="updataLike" flushCache ="true" > update likes set status = #{status} where article_id = #{articleId} and user_id = #{userId} </update > <select id ="getArticleData" resultType ="cn.goitman.pojo.Article" > select article_id,Like_count from article where article_id = #{articleId} </select > <insert id ="saveArticle" flushCache ="true" > insert into article (article_id,Like_count) values (#{articleId},#{LikeCount}) </insert > <update id ="updataArticle" flushCache ="true" > update article set Like_count = #{LikeCount} where article_id = #{articleId} </update > </mapper >
定时任务 Scheduled方式 引导类开启Scheduling注解
package cn.goitman;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.scheduling.annotation.EnableScheduling;@SpringBootApplication @EnableScheduling public class RedisLikeDesignApplication { public static void main (String[] args) { SpringApplication.run(RedisLikeDesignApplication.class, args); } }
package cn.goitman.scheduling;import cn.goitman.service.LikeService;import cn.goitman.task.LikeTask;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;@Component public class LikeScheduling { private static Logger log = LoggerFactory.getLogger(LikeScheduling.class); @Autowired private LikeService likeService; @Scheduled(cron = "0 0/1 * * * ? ") public void likeCron () { log.info("Scheduled 定时任务.........开始........." ); likeService.savaLikeData2DB(); likeService.saveArticleLikeCount2DB(); log.info("Scheduled 定时任务.........结束........." ); } }
Quartz方式
package cn.goitman.config;import cn.goitman.task.LikeTask;import org.quartz.*;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration public class QuartzConfig { private static final String LIKE_TASK_IDENTITY = "LikeTask" ; @Bean public JobDetail jobDatail () { return JobBuilder.newJob(LikeTask.class) .withIdentity(LIKE_TASK_IDENTITY) .storeDurably() .build(); } @Bean public Trigger trigger () { SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInMinutes(1 ) .repeatForever(); return TriggerBuilder.newTrigger() .forJob(jobDatail()) .withIdentity(LIKE_TASK_IDENTITY) .withSchedule(simpleScheduleBuilder) .build(); } }
package cn.goitman.task;import cn.goitman.listener.ApplicationListens;import cn.goitman.service.LikeService;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.quartz.QuartzJobBean;public class LikeTask extends QuartzJobBean { private static Logger log = LoggerFactory.getLogger(LikeTask.class); @Autowired LikeService likeService; @Override protected void executeInternal (JobExecutionContext jobExecutionContext) throws JobExecutionException { log.info("Quartz定时任务.........开始........." ); likeService.savaLikeData2DB(); likeService.saveArticleLikeCount2DB(); log.info("Quartz定时任务.........结束........." ); } }
两种方法任选其一即可,完全看眼缘啦……
钩子函数 在项目开发或运行中,可能会遇到如随应用启动后或关闭前处理某些逻辑
、服务器突然断电(指有备用电缓冲下)
防止数据丢失等的场景,这时钩子函数
(回调函数)起到了决定性作用
package cn.goitman.listener;import cn.goitman.service.LikeService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.DisposableBean;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.CommandLineRunner;import org.springframework.stereotype.Component;public class ApplicationListens implements CommandLineRunner , DisposableBean { private static Logger log = LoggerFactory.getLogger(ApplicationListens.class); @Autowired private LikeService likeService; @Override public void run (String... args) throws Exception { } @Override public void destroy () throws Exception { log.info("程序关闭,钩子回调.........开始........." ); likeService.savaLikeData2DB(); likeService.saveArticleLikeCount2DB(); log.info("程序关闭,钩子回调.........结束........." ); } }
源码地址:https://github.com/wangdaicong/spring-boot-project/tree/master/redisLikeDesign-demo